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

    /*
     * usage: every string should be translated either with _lt if they are registered with a registry at
     *  the load of the app or with Spreadsheet._t in the templates. Spreadsheet._t is exposed in the
     *  sub-env of Spreadsheet components as _t
     * */
    // define a mock translation function, when o-spreadsheet runs in standalone it doesn't translate any string
    let _translate = (s) => s;
    function sprintf(s, ...values) {
        if (values.length === 1 && typeof values[0] === "object") {
            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
     */
    function setTranslationMethod(tfn) {
        _translate = tfn;
    }
    const _t = function (s, ...values) {
        return sprintf(_translate(s), ...values);
    };
    const _lt = function (str, ...values) {
        // casts the object to unknown then to string to trick typescript into thinking that the object it receives is actually a string
        // this way it will be typed correctly (behaves like a string) but tests like typeof _lt("whatever") will be object and not string !
        return new LazyTranslatedString(str, values);
    };
    class LazyTranslatedString extends String {
        constructor(str, values) {
            super(str);
            this.values = values;
        }
        valueOf() {
            const str = super.valueOf();
            return sprintf(_translate(str), ...this.values);
        }
        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 {
        constructor(errorType, message, logLevel = CellErrorLevel.error) {
            super(message);
            this.errorType = errorType;
            this.logLevel = logLevel;
        }
    }
    class BadExpressionError extends EvaluationError {
        constructor(errorMessage) {
            super(CellErrorType.BadExpression, errorMessage);
        }
    }
    class CircularDependencyError extends EvaluationError {
        constructor() {
            super(CellErrorType.CircularDependency, _lt("Circular reference"));
        }
    }
    class InvalidReferenceError extends EvaluationError {
        constructor() {
            super(CellErrorType.InvalidReference, _lt("Invalid reference"));
        }
    }
    class NotAvailableError extends EvaluationError {
        constructor() {
            super(CellErrorType.NotAvailable, _lt("Data not available"), CellErrorLevel.silent);
        }
    }
    class UnknownFunctionError extends EvaluationError {
        constructor(fctName) {
            super(CellErrorType.UnknownFunction, _lt('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 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";
    // 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 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 FILTER_ICON_MARGIN = 2;
    const FILTER_ICON_EDGE_LENGTH = 17;
    // Menus
    const MENU_WIDTH = 250;
    const MENU_ITEM_HEIGHT = 28;
    const MENU_SEPARATOR_BORDER_WIDTH = 1;
    const MENU_SEPARATOR_PADDING = 5;
    const MENU_SEPARATOR_HEIGHT = MENU_SEPARATOR_BORDER_WIDTH + 2 * MENU_SEPARATOR_PADDING;
    const FIGURE_BORDER_SIZE = 1;
    // Fonts
    const DEFAULT_FONT_WEIGHT = "400";
    const DEFAULT_FONT_SIZE = 10;
    const HEADER_FONT_SIZE = 11;
    const DEFAULT_FONT = "'Roboto', arial";
    // Borders
    const DEFAULT_BORDER_DESC = ["thin", "#000"];
    const DEFAULT_FILTER_BORDER_DESC = ["thin", FILTERS_COLOR];
    // DateTimeRegex
    const DATETIME_FORMAT = /[ymdhs:]/;
    // 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;
    // 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 LINE_FILL_TRANSPARENCY = 0.4;
    const MIN_FIG_SIZE = 80;
    // session
    const DEBOUNCE_TIME = 200;
    const MESSAGE_VERSION = 1;
    // Sheets
    const FORBIDDEN_SHEET_CHARS = ["'", "*", "?", "/", "\\", "[", "]"];
    const FORBIDDEN_IN_EXCEL_REGEX = /'|\*|\?|\/|\\|\[|\]/;
    // Cells
    const NULL_FORMAT = undefined;
    const FORMULA_REF_IDENTIFIER = "|";
    const LOADING = "Loading...";
    const DEFAULT_ERROR_MESSAGE = _lt("Invalid expression");
    // Components
    var ComponentsImportance;
    (function (ComponentsImportance) {
        ComponentsImportance[ComponentsImportance["Grid"] = 0] = "Grid";
        ComponentsImportance[ComponentsImportance["Highlight"] = 5] = "Highlight";
        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["ColorPicker"] = 25] = "ColorPicker";
        ComponentsImportance[ComponentsImportance["IconPicker"] = 25] = "IconPicker";
        ComponentsImportance[ComponentsImportance["TopBarComposer"] = 30] = "TopBarComposer";
        ComponentsImportance[ComponentsImportance["Popover"] = 35] = "Popover";
        ComponentsImportance[ComponentsImportance["ChartAnchor"] = 1000] = "ChartAnchor";
    })(ComponentsImportance || (ComponentsImportance = {}));
    const DEFAULT_SHEETVIEW_SIZE = 1000;
    const MAXIMAL_FREEZABLE_RATIO = 0.85;

    const fontSizes = [
        { pt: 7.5, px: 10 },
        { pt: 8, px: 11 },
        { pt: 9, px: 12 },
        { pt: 10, px: 13 },
        { pt: 10.5, px: 14 },
        { pt: 11, px: 15 },
        { pt: 12, px: 16 },
        { pt: 14, px: 18.7 },
        { pt: 15, px: 20 },
        { pt: 16, px: 21.3 },
        { pt: 18, px: 24 },
        { pt: 22, px: 29.3 },
        { pt: 24, px: 32 },
        { pt: 26, px: 34.7 },
        { pt: 36, px: 48 },
    ];
    const fontSizeMap = {};
    for (let font of fontSizes) {
        fontSizeMap[font.pt] = font.px;
    }

    // -----------------------------------------------------------------------------
    // 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 {
        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();
        }
        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) {
            this.jsDate.setFullYear(year);
        }
        setDate(date) {
            this.jsDate.setUTCDate(date);
        }
        setHours(hours) {
            this.jsDate.setUTCHours(hours);
        }
        setMinutes(minutes) {
            this.jsDate.setUTCMinutes(minutes);
        }
        setSeconds(seconds) {
            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 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 timeRegexp = /((\d+(:\d+)?(:\d+)?\s*(AM|PM))|(\d+:\d+(:\d+)?))$/;
    const CACHE = {};
    function parseDateTime(str) {
        if (CACHE[str]) {
            return CACHE[str];
        }
        const date = _parseDateTime(str);
        CACHE[str] = date;
        return date;
    }
    function _parseDateTime(str) {
        str = str.trim();
        let time;
        const timeMatch = str.match(timeRegexp);
        if (timeMatch) {
            time = parseTime(timeMatch[0]);
            if (time === null) {
                return null;
            }
            str = str.replace(timeMatch[0], "").trim();
        }
        let date;
        const mdyDateMatch = str.match(mdyDateRegexp);
        const ymdDateMatch = str.match(ymdDateRegexp);
        if (mdyDateMatch || ymdDateMatch) {
            let dateMatch;
            if (mdyDateMatch) {
                dateMatch = mdyDateMatch[0];
                date = parseDate(dateMatch, "mdy");
            }
            else {
                dateMatch = ymdDateMatch[0];
                date = parseDate(dateMatch, "ymd");
            }
            if (date === null) {
                return null;
            }
            str = str.replace(dateMatch, "").trim();
        }
        if (str !== "" || !(date || time)) {
            return null;
        }
        if (date && time) {
            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;
    }
    function parseDate(str, dateFormat) {
        const isMDY = dateFormat === "mdy";
        const isYMD = dateFormat === "ymd";
        if (isMDY || isYMD) {
            const parts = str.split(/\/|-|\s/);
            const monthIndex = isMDY ? 0 : 1;
            const dayIndex = isMDY ? 1 : 2;
            const yearIndex = isMDY ? 2 : 0;
            const month = Number(parts[monthIndex]);
            const day = Number(parts[dayIndex]);
            const leadingZero = (parts[monthIndex].length === 2 && month < 10) || (parts[dayIndex].length === 2 && day < 10);
            const year = parts[yearIndex] ? inferYear(parts[yearIndex]) : CURRENT_YEAR;
            const jsDate = new DateTime(year, month - 1, day);
            const sep = str.match(/\/|-|\s/)[0];
            if (jsDate.getMonth() !== month - 1 || jsDate.getDate() !== day) {
                // invalid date
                return null;
            }
            const delta = jsDate.getTime() - INITIAL_1900_DAY.getTime();
            let format = leadingZero ? `mm${sep}dd` : `m${sep}d`;
            if (parts[yearIndex]) {
                format = isMDY ? format + sep + "yyyy" : "yyyy" + sep + format;
            }
            return {
                value: Math.round(delta / MS_PER_DAY),
                format: format,
                jsDate,
            };
        }
        return null;
    }
    function inferYear(str) {
        const nbr = Number(str);
        switch (str.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 0;
    }
    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) {
        const delta = date.getTime() - INITIAL_1900_DAY.getTime();
        return Math.round(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;
    }

    //------------------------------------------------------------------------------
    /**
     * 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 === null || obj === void 0 ? void 0 : 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 getComposerSheetName(sheetName) {
        var _a;
        if (((_a = sheetName.match(/\w/g)) === null || _a === void 0 ? void 0 : _a.length) !== sheetName.length) {
            sheetName = `'${sheetName}'`;
        }
        return sheetName;
    }
    function clip(val, min, max) {
        return val < min ? min : val > max ? max : val;
    }
    /** Get the default height of the cell. The height depends on the font size and
     * the number of broken line text in the cell */
    function getDefaultCellHeight(style, numberOfLines) {
        if (!(style === null || style === void 0 ? void 0 : style.fontSize)) {
            return DEFAULT_CELL_HEIGHT;
        }
        return ((numberOfLines || 1) * (computeTextFontSizeInPixels(style) + MIN_CELL_TEXT_MARGIN) -
            MIN_CELL_TEXT_MARGIN +
            2 * PADDING_AUTORESIZE_VERTICAL);
    }
    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];
    }
    const textWidthCache = {};
    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;
        if (!fontSizeMap[sizeInPt]) {
            throw new Error("Size of the font is not supported");
        }
        return fontSizeMap[sizeInPt];
    }
    /**
     * 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;
    }
    /**
     * 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";
    }
    function isDateTime(str) {
        return parseDateTime(str) !== null;
    }
    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 isMarkdownSheetLink(str) {
        if (!isMarkdownLink(str)) {
            return false;
        }
        const { url } = parseMarkdownLink(str);
        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 parseSheetLink(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 isNonEmptyString(str) {
        return str !== undefined && str !== "";
    }
    /**
     * 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 (let [key, value] of Object.entries(itemsDic)) {
            if (deepEquals(value, 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 : largeMax(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 false;
        // Objects can have different keys if the values are undefined
        const keys = new Set();
        Object.keys(o1).forEach((key) => keys.add(key));
        Object.keys(o2).forEach((key) => keys.add(key));
        for (let key of keys) {
            if (typeof o1[key] !== typeof o1[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.
     * It makes the assumption that both array do not contain duplicates.
     */
    function includesAll(arr, values) {
        if (arr.length < values.length) {
            return false;
        }
        const set = new Set(arr);
        return values.every((value) => set.has(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;
    }
    /** Transform a string to lower case. If the string is undefined, return an empty string */
    function toLowerCase(str) {
        return str ? str.toLowerCase() : "";
    }
    function transpose2dArray(matrix) {
        if (!matrix.length)
            return matrix;
        return matrix[0].map((_, i) => matrix.map((row) => row[i]));
    }
    /**
     * 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];
    }
    /**
     * Alternative to Math.max that works with large arrays.
     * Typically useful for arrays bigger than 100k elements.
     */
    function largeMax(array) {
        let len = array.length;
        if (len < 100000)
            return Math.max(...array);
        let max = -Infinity;
        while (len--) {
            max = array[len] > max ? array[len] : max;
        }
        return max;
    }
    /**
     * Alternative to Math.min that works with large arrays.
     * Typically useful for arrays bigger than 100k elements.
     */
    function largeMin(array) {
        let len = array.length;
        if (len < 100000)
            return Math.min(...array);
        let min = +Infinity;
        while (len--) {
            min = array[len] < min ? array[len] : min;
        }
        return min;
    }

    const RBA_REGEX = /rgba?\(|\s+|\)/gi;
    const HEX_MATCH = /^#([A-F\d]{2}){3,4}$/g;
    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"));
    }
    let colorIndex = 0;
    function getNextColor() {
        colorIndex = ++colorIndex % colors$1.length;
        return colors$1[colorIndex];
    }
    /**
     * 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 (!hexColor.match(HEX_MATCH)) {
            throw new Error(`invalid color input: ${color}`);
        }
        return hexColor;
    }
    function isColorValid(color) {
        try {
            toHex(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 isSameColor(color1, color2) {
        return isColorValid(color1) && isColorValid(color2) && toHex(color1) === toHex(color2);
    }

    /** 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 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);
        }
    }
    /**
     * Convert a string (describing a column) to its number value.
     *
     * Examples:
     *     'A' => 0
     *     'Z' => 25
     *     'AA' => 26
     */
    function lettersToNumber(letters) {
        let result = 0;
        const l = letters.length;
        for (let i = 0; i < l; i++) {
            let n = letters.charCodeAt(i) - 65 + (i < l - 1 ? 1 : 0);
            result += n * 26 ** (l - i - 1);
        }
        return result;
    }
    /**
     * 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.toUpperCase().trim();
        const match = xc.match(cellReference);
        if (match !== null) {
            const [m, letters, numbers] = match;
            if (m === xc) {
                const col = lettersToNumber(letters);
                const row = parseInt(numbers, 10) - 1;
                return { col, row };
            }
        }
        throw new Error(`Invalid cell description: ${xc}`);
    }
    /**
     * 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));
    }

    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;
    //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: _lt("January"),
        1: _lt("February"),
        2: _lt("March"),
        3: _lt("April"),
        4: _lt("May"),
        5: _lt("June"),
        6: _lt("July"),
        7: _lt("August"),
        8: _lt("September"),
        9: _lt("October"),
        10: _lt("November"),
        11: _lt("December"),
    };
    const DAYS$1 = {
        0: _lt("Sunday"),
        1: _lt("Monday"),
        2: _lt("Tuesday"),
        3: _lt("Wednesday"),
        4: _lt("Thursday"),
        5: _lt("Friday"),
        6: _lt("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) {
        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);
            case "object":
                return "0";
        }
    }
    function applyInternalFormat(value, internalFormat) {
        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);
                    break;
                case "STRING":
                    formattedValue += part.format;
                    break;
            }
        }
        return formattedValue;
    }
    function applyInternalNumberFormat(value, format) {
        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);
        if (format.decimalPart !== undefined) {
            formattedValue += "." + applyDecimalFormat(decimalDigits || "", format.decimalPart);
        }
        if (format.isPercent) {
            formattedValue += "%";
        }
        return formattedValue;
    }
    function applyIntegerFormat(integerDigits, integerFormat, hasSeparator) {
        var _a;
        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 (hasSeparator) {
            formattedInteger = ((_a = formattedInteger.match(thousandsGroupsRegexp)) === null || _a === void 0 ? void 0 : _a.join(",")) || 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) {
        var _a;
        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 = ((_a = slicedDecimalDigits.match(leadingZeroesRegexp)) === null || _a === void 0 ? void 0 : _a[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 };
    }
    /**
     * Check if the given format is a time, date or date time format.
     */
    function isDateTimeFormat(format) {
        try {
            applyDateTimeFormat(1, format);
            return true;
        }
        catch (error) {
            return false;
        }
    }
    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) {
        var _a;
        const sep = (_a = format.match(/\/|-|\s/)) === null || _a === void 0 ? void 0 : _a[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);
    }
    // -----------------------------------------------------------------------------
    // CREATE / MODIFY FORMAT
    // -----------------------------------------------------------------------------
    function createDefaultFormat(value) {
        let { decimalDigits } = splitNumber(value, 10);
        return decimalDigits ? "0." + "0".repeat(decimalDigits.length) : "0";
    }
    function createLargeNumberFormat(format, magnitude, postFix) {
        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 === null || nextFormatPart === void 0 ? void 0 : nextFormatPart.type) === "STRING" && ["k", "m", "b"].includes(nextFormatPart.format)) {
                i++;
            }
        }
        return convertInternalFormatToFormat(largeNumberFormat);
    }
    function changeDecimalPlaces(format, step) {
        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) {
        var _a;
        const _format = { ...format };
        const sign = Math.sign(step);
        const decimalLength = ((_a = _format.decimalPart) === null || _a === void 0 ? void 0 : _a.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 (subFormat.match(DATETIME_FORMAT)) {
                    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) {
        var _a;
        format = format.trim();
        if (containsInvalidNumberChars(format)) {
            throw new Error(`Invalid number format: ${format}`);
        }
        const isPercent = format.includes("%");
        const magnitudeCommas = ((_a = format.match(magnitudeRegex)) === null || _a === void 0 ? void 0 : _a[0]) || "";
        const magnitude = !magnitudeCommas ? 1 : 1000 ** magnitudeCommas.length;
        let _format = format.slice(0, format.length - (magnitudeCommas.length || 0));
        const thousandsSeparator = _format.includes(",");
        if (_format.match(/\..*,/)) {
            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;
    }

    /**
     * This regexp 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 accept "," as thousand separator, because when we tokenize a
     *   formula, commas are used to separate arguments
     * - it does not support % symbol, in formulas % is an operator
     */
    const formulaNumberRegexp = /(^-?\d+(\.?\d*(e(\+|-)?\d+)?)?|^-?\.\d+)(?!\w|!)/;
    const pIntegerAndDecimals = "(\\d+(,\\d{3,})*(\\.\\d*)?)"; // pattern that match integer number with or without decimal digits
    const pOnlyDecimals = "(\\.\\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 true if the argument is a "number string".
     *
     * Note that "" (empty string) does not count as a number string
     */
    function isNumber(value) {
        if (!value)
            return false;
        // TO DO: add regexp for DATE string format (ex match: "28 02 2020")
        return numberRegexp.test(value.trim());
    }
    const invaluableSymbolsRegexp = /[,\$€]+/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) {
        // remove invaluable characters
        str = str.replace(invaluableSymbolsRegexp, "");
        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));
    }

    class RangeImpl {
        constructor(args, getSheetSize) {
            this.getSheetSize = getSheetSize;
            this.prefixSheet = false;
            this._zone = args.zone;
            this.parts = args.parts;
            this.prefixSheet = args.prefixSheet;
            this.invalidXc = args.invalidXc;
            this.sheetId = args.sheetId;
            this.invalidSheetName = args.invalidSheetName;
        }
        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).height - 1 };
            }
            else if (right === undefined && bottom !== undefined) {
                return { bottom, left, top, right: this.getSheetSize(this.sheetId).width - 1 };
            }
            throw new Error(_lt("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
         * (in place!)
         */
        orderZone() {
            if (this._zone.right !== undefined && this._zone.right < this._zone.left) {
                let right = this._zone.right;
                this._zone.right = this._zone.left;
                this._zone.left = right;
                let rightFixed = this.parts[1].colFixed;
                this.parts[1].colFixed = this.parts[0].colFixed;
                this.parts[0].colFixed = rightFixed;
            }
            if (this._zone.bottom !== undefined && this._zone.bottom < this._zone.top) {
                let bottom = this._zone.bottom;
                this._zone.bottom = this._zone.top;
                this._zone.top = bottom;
                let bottomFixed = this.parts[1].rowFixed;
                this.parts[1].rowFixed = this.parts[0].rowFixed;
                this.parts[0].rowFixed = bottomFixed;
            }
        }
        /**
         *
         * @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 === null || rangeParams === void 0 ? void 0 : rangeParams.zone) ? rangeParams.zone : { ...this._zone },
                sheetId: (rangeParams === null || rangeParams === void 0 ? void 0 : 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 === null || rangeParams === void 0 ? void 0 : rangeParams.parts)
                    ? rangeParams.parts
                    : this.parts.map((part) => {
                        return { rowFixed: part.rowFixed, colFixed: part.colFixed };
                    }),
                prefixSheet: (rangeParams === null || rangeParams === void 0 ? void 0 : 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 createValidRange(getters, sheetId, xc) {
        if (!xc)
            return;
        const range = getters.getRangeFromSheetXC(sheetId, xc);
        return !(range.invalidSheetName || range.invalidXc) ? range : undefined;
    }

    /** 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 isSheetNameEqual(name1, name2) {
        if (name1 === undefined || name2 === undefined) {
            return false;
        }
        return (getUnquotedSheetName(name1.trim().toUpperCase()) ===
            getUnquotedSheetName(name2.trim().toUpperCase()));
    }
    function getNextSheetName(existingNames, baseName = "Sheet") {
        let i = 1;
        let name = `${baseName}${i}`;
        while (existingNames.includes(name)) {
            name = `${baseName}${i}`;
            i++;
        }
        return name;
    }
    function getDuplicateSheetName(nameToDuplicate, existingNames) {
        let i = 1;
        const baseName = _lt("Copy of %s", nameToDuplicate);
        let name = baseName.toString();
        while (existingNames.includes(name)) {
            name = `${baseName} (${i})`;
            i++;
        }
        return name;
    }

    /*
     * https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
     * */
    class UuidGenerator {
        constructor() {
            this.isFastIdStrategy = false;
            this.fastIdStart = 0;
        }
        setIsFastStrategy(isFast) {
            this.isFastIdStrategy = isFast;
        }
        /**
         * Generates a custom UUID using a simple 26^8 method (8-character alphanumeric string with lowercase letters)
         * This has a higher chance of collision than a UUIDv4, but not only faster to generate than an UUIDV4,
         * it also has a smaller size, which is preferable to alleviate the overall data size.
         *
         * This method is preferable when generating uuids for the core data (sheetId, figureId, etc)
         * as they will appear several times in the revisions and local history.
         *
         */
        smallUuid() {
            if (this.isFastIdStrategy) {
                this.fastIdStart++;
                return String(this.fastIdStart);
            }
            else if (window.crypto) {
                return "10000000-1000".replace(/[01]/g, (c) => {
                    const n = Number(c);
                    return (n ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (n / 4)))).toString(16);
                });
            }
            else {
                // mainly for jest and other browsers that do not have the crypto functionality
                return "xxxxxxxx-xxxx".replace(/[xy]/g, function (c) {
                    var r = (Math.random() * 16) | 0, v = c == "x" ? r : (r & 0x3) | 0x8;
                    return v.toString(16);
                });
            }
        }
        /**
         * Generates an UUIDV4, has astronomically low chance of collision, but is larger in size than the smallUuid.
         * This method should be used when you need to avoid collisions at all costs, like the id of a revision.
         */
        uuidv4() {
            if (this.isFastIdStrategy) {
                this.fastIdStart++;
                return String(this.fastIdStart);
            }
            else if (window.crypto) {
                return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) => {
                    const n = Number(c);
                    return (n ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (n / 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);
                });
            }
        }
    }

    /**
     * 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) {
        xc = xc.split("!").pop();
        const ranges = xc
            .replace(/\$/g, "")
            .split(":")
            .map((x) => x.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);
        reorderZone(zone);
        return zone;
    }
    function reorderZone(zone) {
        if (zone.right !== undefined && zone.right < zone.left) {
            const right = zone.left;
            zone.left = zone.right;
            zone.right = right;
        }
        if (zone.bottom !== undefined && zone.bottom < zone.top) {
            const bottom = zone.top;
            zone.top = zone.bottom;
            zone.bottom = bottom;
        }
    }
    /**
     * 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 ((zone.bottom === undefined || (zone.bottom >= zone.top && zone.bottom >= 0)) &&
            (zone.right === undefined || (zone.right >= zone.left && zone.right >= 0)) &&
            zone.top >= 0 &&
            zone.left >= 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(_lt("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(toZone);
        // 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 {
            height: zone.bottom - zone.top + 1,
            width: zone.right - zone.left + 1,
        };
    }
    function isOneDimensional(zone) {
        const { width, height } = zoneToDimension(zone);
        return width === 1 || height === 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;
    }
    /**
     * 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;
    }

    /**
     * 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 {
        constructor() {
            this.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];
        }
    }

    /**
     * This registry is intended to map a cell content (raw string) to
     * an instance of a cell.
     */
    const chartRegistry = new Registry();
    const chartComponentRegistry = new Registry();

    class ChartJsComponent extends owl.Component {
        constructor() {
            super(...arguments);
            this.canvas = owl.useRef("graphContainer");
        }
        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);
            });
            let previousRuntime = this.chartRuntime;
            owl.onPatched(() => {
                const chartRuntime = this.chartRuntime;
                if (deepEquals(previousRuntime, chartRuntime)) {
                    return;
                }
                this.updateChartJs(chartRuntime);
                previousRuntime = chartRuntime;
            });
        }
        createChart(chartData) {
            const canvas = this.canvas.el;
            const ctx = canvas.getContext("2d");
            this.chart = new window.Chart(ctx, chartData);
        }
        updateChartJs(chartRuntime) {
            var _a, _b, _c, _d;
            const chartData = chartRuntime.chartJsConfig;
            if (chartData.data && chartData.data.datasets) {
                this.chart.data = chartData.data;
                if ((_a = chartData.options) === null || _a === void 0 ? void 0 : _a.title) {
                    this.chart.config.options.title = chartData.options.title;
                }
                if (chartData.options && "valueLabel" in chartData.options) {
                    if ((_b = chartData.options) === null || _b === void 0 ? void 0 : _b.valueLabel) {
                        this.chart.config.options.valueLabel =
                            chartData.options.valueLabel;
                    }
                }
            }
            else {
                this.chart.data.datasets = undefined;
            }
            this.chart.config.options.legend = (_c = chartData.options) === null || _c === void 0 ? void 0 : _c.legend;
            this.chart.config.options.scales = (_d = chartData.options) === null || _d === void 0 ? void 0 : _d.scales;
            this.chart.update({ duration: 0 });
        }
    }
    ChartJsComponent.template = "o-spreadsheet-ChartJsComponent";
    chartComponentRegistry.add("line", ChartJsComponent);
    chartComponentRegistry.add("bar", ChartJsComponent);
    chartComponentRegistry.add("pie", ChartJsComponent);
    chartComponentRegistry.add("gauge", ChartJsComponent);

    /**
     * An AutofillModifierImplementation is used to describe how to handle a
     * AutofillModifier.
     */
    const autofillModifiersRegistry = new Registry();
    autofillModifiersRegistry
        .add("INCREMENT_MODIFIER", {
        apply: (rule, data) => {
            var _a;
            rule.current += rule.increment;
            const content = rule.current.toString();
            const tooltipValue = formatValue(rule.current, (_a = data.cell) === null || _a === void 0 ? void 0 : _a.format);
            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) => {
            var _a, _b;
            const content = ((_a = data.cell) === null || _a === void 0 ? void 0 : _a.content) || "";
            return {
                cellData: {
                    border: data.border,
                    style: data.cell && data.cell.style,
                    format: data.cell && data.cell.format,
                    content,
                },
                tooltip: content ? { props: { content: (_b = data.cell) === null || _b === void 0 ? void 0 : _b.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;
            }
            if (!data.cell || !data.cell.isFormula()) {
                return { cellData: {} };
            }
            const sheetId = data.sheetId;
            const ranges = getters.createAdaptedRanges(data.cell.dependencies, x, y, sheetId);
            const content = getters.buildFormulaContent(sheetId, data.cell, ranges);
            return {
                cellData: {
                    border: data.border,
                    style: data.cell.style,
                    format: data.cell.format,
                    content,
                },
                tooltip: content ? { props: { content } } : undefined,
            };
        },
    });

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

    function isSheetDependent(cmd) {
        return "sheetId" in cmd;
    }
    function isGridDependent(cmd) {
        return "dimension" in cmd && "sheetId" in cmd;
    }
    function isTargetDependent(cmd) {
        return "target" in cmd && "sheetId" in cmd;
    }
    function isRangeDependant(cmd) {
        return "ranges" 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",
        "DELETE_CELL",
        "INSERT_CELL",
        "UNDO",
        "REDO",
    ]);
    const invalidateCFEvaluationCommands = new Set([
        ...invalidateEvaluationCommands,
        "DUPLICATE_SHEET",
        "EVALUATE_CELLS",
        "ADD_CONDITIONAL_FORMAT",
        "REMOVE_CONDITIONAL_FORMAT",
        "MOVE_CONDITIONAL_FORMAT",
    ]);
    const readonlyAllowedCommands = new Set([
        "START",
        "ACTIVATE_SHEET",
        "COPY",
        "PREPARE_SELECTION_INPUT_EXPANSION",
        "STOP_SELECTION_INPUT",
        "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",
        "MOVE_CONDITIONAL_FORMAT",
        /** FIGURES */
        "CREATE_FIGURE",
        "DELETE_FIGURE",
        "UPDATE_FIGURE",
        /** FORMATTING */
        "SET_FORMATTING",
        "CLEAR_FORMATTING",
        "SET_BORDER",
        /** CHART */
        "CREATE_CHART",
        "UPDATE_CHART",
        /** FILTERS */
        "CREATE_FILTER_TABLE",
        "REMOVE_FILTER_TABLE",
    ]);
    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 {
        constructor(results = []) {
            if (!Array.isArray(results)) {
                results = [results];
            }
            results = [...new Set(results)];
            this.reasons = results.filter((result) => result !== 0 /* 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[CommandResult["Success"] = 0] = "Success";
        CommandResult[CommandResult["CancelledForUnknownReason"] = 1] = "CancelledForUnknownReason";
        CommandResult[CommandResult["WillRemoveExistingMerge"] = 2] = "WillRemoveExistingMerge";
        CommandResult[CommandResult["MergeIsDestructive"] = 3] = "MergeIsDestructive";
        CommandResult[CommandResult["CellIsMerged"] = 4] = "CellIsMerged";
        CommandResult[CommandResult["InvalidTarget"] = 5] = "InvalidTarget";
        CommandResult[CommandResult["EmptyUndoStack"] = 6] = "EmptyUndoStack";
        CommandResult[CommandResult["EmptyRedoStack"] = 7] = "EmptyRedoStack";
        CommandResult[CommandResult["NotEnoughElements"] = 8] = "NotEnoughElements";
        CommandResult[CommandResult["NotEnoughSheets"] = 9] = "NotEnoughSheets";
        CommandResult[CommandResult["MissingSheetName"] = 10] = "MissingSheetName";
        CommandResult[CommandResult["DuplicatedSheetName"] = 11] = "DuplicatedSheetName";
        CommandResult[CommandResult["DuplicatedSheetId"] = 12] = "DuplicatedSheetId";
        CommandResult[CommandResult["ForbiddenCharactersInSheetName"] = 13] = "ForbiddenCharactersInSheetName";
        CommandResult[CommandResult["WrongSheetMove"] = 14] = "WrongSheetMove";
        CommandResult[CommandResult["WrongSheetPosition"] = 15] = "WrongSheetPosition";
        CommandResult[CommandResult["InvalidAnchorZone"] = 16] = "InvalidAnchorZone";
        CommandResult[CommandResult["SelectionOutOfBound"] = 17] = "SelectionOutOfBound";
        CommandResult[CommandResult["TargetOutOfSheet"] = 18] = "TargetOutOfSheet";
        CommandResult[CommandResult["WrongCutSelection"] = 19] = "WrongCutSelection";
        CommandResult[CommandResult["WrongPasteSelection"] = 20] = "WrongPasteSelection";
        CommandResult[CommandResult["WrongPasteOption"] = 21] = "WrongPasteOption";
        CommandResult[CommandResult["WrongFigurePasteOption"] = 22] = "WrongFigurePasteOption";
        CommandResult[CommandResult["EmptyClipboard"] = 23] = "EmptyClipboard";
        CommandResult[CommandResult["EmptyRange"] = 24] = "EmptyRange";
        CommandResult[CommandResult["InvalidRange"] = 25] = "InvalidRange";
        CommandResult[CommandResult["InvalidZones"] = 26] = "InvalidZones";
        CommandResult[CommandResult["InvalidSheetId"] = 27] = "InvalidSheetId";
        CommandResult[CommandResult["InvalidCellId"] = 28] = "InvalidCellId";
        CommandResult[CommandResult["InputAlreadyFocused"] = 29] = "InputAlreadyFocused";
        CommandResult[CommandResult["MaximumRangesReached"] = 30] = "MaximumRangesReached";
        CommandResult[CommandResult["InvalidInputId"] = 31] = "InvalidInputId";
        CommandResult[CommandResult["InvalidChartDefinition"] = 32] = "InvalidChartDefinition";
        CommandResult[CommandResult["InvalidDataSet"] = 33] = "InvalidDataSet";
        CommandResult[CommandResult["InvalidLabelRange"] = 34] = "InvalidLabelRange";
        CommandResult[CommandResult["InvalidScorecardKeyValue"] = 35] = "InvalidScorecardKeyValue";
        CommandResult[CommandResult["InvalidScorecardBaseline"] = 36] = "InvalidScorecardBaseline";
        CommandResult[CommandResult["InvalidGaugeDataRange"] = 37] = "InvalidGaugeDataRange";
        CommandResult[CommandResult["EmptyGaugeRangeMin"] = 38] = "EmptyGaugeRangeMin";
        CommandResult[CommandResult["GaugeRangeMinNaN"] = 39] = "GaugeRangeMinNaN";
        CommandResult[CommandResult["EmptyGaugeRangeMax"] = 40] = "EmptyGaugeRangeMax";
        CommandResult[CommandResult["GaugeRangeMaxNaN"] = 41] = "GaugeRangeMaxNaN";
        CommandResult[CommandResult["GaugeRangeMinBiggerThanRangeMax"] = 42] = "GaugeRangeMinBiggerThanRangeMax";
        CommandResult[CommandResult["GaugeLowerInflectionPointNaN"] = 43] = "GaugeLowerInflectionPointNaN";
        CommandResult[CommandResult["GaugeUpperInflectionPointNaN"] = 44] = "GaugeUpperInflectionPointNaN";
        CommandResult[CommandResult["GaugeLowerBiggerThanUpper"] = 45] = "GaugeLowerBiggerThanUpper";
        CommandResult[CommandResult["InvalidAutofillSelection"] = 46] = "InvalidAutofillSelection";
        CommandResult[CommandResult["WrongComposerSelection"] = 47] = "WrongComposerSelection";
        CommandResult[CommandResult["MinBiggerThanMax"] = 48] = "MinBiggerThanMax";
        CommandResult[CommandResult["LowerBiggerThanUpper"] = 49] = "LowerBiggerThanUpper";
        CommandResult[CommandResult["MidBiggerThanMax"] = 50] = "MidBiggerThanMax";
        CommandResult[CommandResult["MinBiggerThanMid"] = 51] = "MinBiggerThanMid";
        CommandResult[CommandResult["FirstArgMissing"] = 52] = "FirstArgMissing";
        CommandResult[CommandResult["SecondArgMissing"] = 53] = "SecondArgMissing";
        CommandResult[CommandResult["MinNaN"] = 54] = "MinNaN";
        CommandResult[CommandResult["MidNaN"] = 55] = "MidNaN";
        CommandResult[CommandResult["MaxNaN"] = 56] = "MaxNaN";
        CommandResult[CommandResult["ValueUpperInflectionNaN"] = 57] = "ValueUpperInflectionNaN";
        CommandResult[CommandResult["ValueLowerInflectionNaN"] = 58] = "ValueLowerInflectionNaN";
        CommandResult[CommandResult["MinInvalidFormula"] = 59] = "MinInvalidFormula";
        CommandResult[CommandResult["MidInvalidFormula"] = 60] = "MidInvalidFormula";
        CommandResult[CommandResult["MaxInvalidFormula"] = 61] = "MaxInvalidFormula";
        CommandResult[CommandResult["ValueUpperInvalidFormula"] = 62] = "ValueUpperInvalidFormula";
        CommandResult[CommandResult["ValueLowerInvalidFormula"] = 63] = "ValueLowerInvalidFormula";
        CommandResult[CommandResult["InvalidSortZone"] = 64] = "InvalidSortZone";
        CommandResult[CommandResult["WaitingSessionConfirmation"] = 65] = "WaitingSessionConfirmation";
        CommandResult[CommandResult["MergeOverlap"] = 66] = "MergeOverlap";
        CommandResult[CommandResult["TooManyHiddenElements"] = 67] = "TooManyHiddenElements";
        CommandResult[CommandResult["Readonly"] = 68] = "Readonly";
        CommandResult[CommandResult["InvalidViewportSize"] = 69] = "InvalidViewportSize";
        CommandResult[CommandResult["InvalidScrollingDirection"] = 70] = "InvalidScrollingDirection";
        CommandResult[CommandResult["FigureDoesNotExist"] = 71] = "FigureDoesNotExist";
        CommandResult[CommandResult["InvalidConditionalFormatId"] = 72] = "InvalidConditionalFormatId";
        CommandResult[CommandResult["InvalidCellPopover"] = 73] = "InvalidCellPopover";
        CommandResult[CommandResult["EmptyTarget"] = 74] = "EmptyTarget";
        CommandResult[CommandResult["InvalidFreezeQuantity"] = 75] = "InvalidFreezeQuantity";
        CommandResult[CommandResult["FrozenPaneOverlap"] = 76] = "FrozenPaneOverlap";
        CommandResult[CommandResult["ValuesNotChanged"] = 77] = "ValuesNotChanged";
        CommandResult[CommandResult["InvalidFilterZone"] = 78] = "InvalidFilterZone";
        CommandResult[CommandResult["FilterOverlap"] = 79] = "FilterOverlap";
        CommandResult[CommandResult["FilterNotFound"] = 80] = "FilterNotFound";
        CommandResult[CommandResult["MergeInFilter"] = 81] = "MergeInFilter";
        CommandResult[CommandResult["NonContinuousTargets"] = 82] = "NonContinuousTargets";
        CommandResult[CommandResult["DuplicatedFigureId"] = 83] = "DuplicatedFigureId";
        CommandResult[CommandResult["InvalidSelectionStep"] = 84] = "InvalidSelectionStep";
        CommandResult[CommandResult["DuplicatedChartId"] = 85] = "DuplicatedChartId";
        CommandResult[CommandResult["ChartDoesNotExist"] = 86] = "ChartDoesNotExist";
        CommandResult[CommandResult["InvalidHeaderIndex"] = 87] = "InvalidHeaderIndex";
        CommandResult[CommandResult["InvalidQuantity"] = 88] = "InvalidQuantity";
        CommandResult[CommandResult["NoChanges"] = 89] = "NoChanges";
    })(exports.CommandResult || (exports.CommandResult = {}));

    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["Selection"] = 5] = "Selection";
        LAYERS[LAYERS["Autofill"] = 6] = "Autofill";
        LAYERS[LAYERS["Headers"] = 7] = "Headers";
    })(LAYERS || (LAYERS = {}));

    const autofillRulesRegistry = new Registry();
    /**
     * Get the consecutive xc that are of type "number" or "date".
     * Return the one which contains the given cell
     */
    function getGroup(cell, cells) {
        let group = [];
        let found = false;
        for (let x of cells) {
            if (x === cell) {
                found = true;
            }
            if ((x === null || x === void 0 ? void 0 : x.evaluated.type) === CellValueType.number) {
                group.push(x.evaluated.value);
            }
            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;
    }
    autofillRulesRegistry
        .add("simple_value_copy", {
        condition: (cell, cells) => {
            var _a;
            return cells.length === 1 && !cell.isFormula() && !((_a = cell.format) === null || _a === void 0 ? void 0 : _a.match(DATETIME_FORMAT));
        },
        generateRule: () => {
            return { type: "COPY_MODIFIER" };
        },
        sequence: 10,
    })
        .add("copy_text", {
        condition: (cell) => !cell.isFormula() && cell.evaluated.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.evaluated.type === CellValueType.number,
        generateRule: (cell, cells, direction) => {
            const group = getGroup(cell, cells);
            let increment = 1;
            if (group.length === 1 && ["up", "left"].includes(direction)) {
                increment = -1;
            }
            else if (group.length == 2) {
                increment = (group[1] - group[0]) * 2;
            }
            else if (group.length > 2) {
                increment = getAverageIncrement(group) * group.length;
            }
            return {
                type: "INCREMENT_MODIFIER",
                increment,
                current: cell.evaluated.type === CellValueType.number ? cell.evaluated.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 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;
    }
    function cssPropertiesToCss(attributes) {
        const str = Object.entries(attributes)
            .map(([attName, attValue]) => `${attName}: ${attValue};`)
            .join("\n");
        return "\n" + str + "\n";
    }

    const ERROR_TOOLTIP_HEIGHT = 40;
    const ERROR_TOOLTIP_WIDTH = 180;
    css /* scss */ `
  .o-error-tooltip {
    font-size: 13px;
    background-color: white;
    border-left: 3px solid red;
    padding: 10px;
    overflow-wrap: break-word;
  }
`;
    class ErrorToolTip extends owl.Component {
    }
    ErrorToolTip.size = { width: ERROR_TOOLTIP_WIDTH, height: ERROR_TOOLTIP_HEIGHT };
    ErrorToolTip.template = "o-spreadsheet-ErrorToolTip";
    ErrorToolTip.components = {};
    const ErrorToolTipPopoverBuilder = {
        onHover: (position, getters) => {
            const cell = getters.getCell(getters.getActiveSheetId(), position.col, position.row);
            if ((cell === null || cell === void 0 ? void 0 : cell.evaluated.type) === CellValueType.error &&
                cell.evaluated.error.logLevel > CellErrorLevel.silent) {
                return {
                    isOpen: true,
                    props: { text: cell.evaluated.error.message },
                    Component: ErrorToolTip,
                    cellCorner: "TopRight",
                };
            }
            return { isOpen: false };
        },
    };

    class FilterMenuValueItem extends owl.Component {
        constructor() {
            super(...arguments);
            this.itemRef = owl.useRef("menuValueItem");
        }
        setup() {
            owl.onWillPatch(() => {
                if (this.props.scrolledTo) {
                    this.scrollListToSelectedValue();
                }
            });
        }
        scrollListToSelectedValue() {
            var _a, _b;
            if (!this.itemRef.el) {
                return;
            }
            (_b = (_a = this.itemRef.el).scrollIntoView) === null || _b === void 0 ? void 0 : _b.call(_a, {
                block: this.props.scrolledTo === "bottom" ? "end" : "start",
            });
        }
    }
    FilterMenuValueItem.template = "o-spreadsheet-FilterMenuValueItem";

    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;

      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-value {
        padding: 4px;
        line-height: 20px;
        height: 28px;
        .o-filter-menu-value-checked {
          width: 20px;
        }
      }

      .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 {
        constructor() {
            super(...arguments);
            this.state = owl.useState({
                values: [],
                textFilter: "",
                selectedValue: undefined,
            });
            this.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.col, position.row);
            if (!filter) {
                return [];
            }
            const cellValues = (filter.filteredZone ? positions(filter.filteredZone) : [])
                .filter(({ row }) => !this.env.model.getters.isRowHidden(sheetId, row))
                .map(({ col, row }) => { var _a; return (_a = this.env.model.getters.getCell(sheetId, col, row)) === null || _a === void 0 ? void 0 : _a.formattedValue; });
            const filterValues = this.env.model.getters.getFilterValues(sheetId, position.col, position.row);
            const normalizedFilteredValues = new Set(filterValues.map(toLowerCase));
            const set = new Set();
            const values = [];
            const addValue = (value) => {
                const normalizedValue = toLowerCase(value);
                if (!set.has(normalizedValue)) {
                    values.push({
                        string: value || "",
                        checked: !normalizedFilteredValues.has(normalizedValue),
                        normalizedValue,
                    });
                    set.add(normalizedValue);
                }
            };
            cellValues.forEach(addValue);
            filterValues.forEach(addValue);
            return values.sort((val1, val2) => val1.normalizedValue.localeCompare(val2.normalizedValue, undefined, {
                numeric: true,
                sensitivity: "base",
            }));
        }
        checkValue(value) {
            var _a;
            this.state.selectedValue = value.string;
            value.checked = !value.checked;
            (_a = this.searchBar.el) === null || _a === void 0 ? void 0 : _a.focus();
        }
        onMouseMove(value) {
            this.state.selectedValue = value.string;
        }
        selectAll() {
            this.state.values.forEach((value) => (value.checked = true));
        }
        clearAll() {
            this.state.values.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.col, position.row);
        }
        get displayedValues() {
            if (!this.state.textFilter) {
                return this.state.values;
            }
            return fuzzyLookup(this.state.textFilter, this.state.values, (val) => val.string);
        }
        confirm() {
            var _a, _b;
            const position = this.props.filterPosition;
            this.env.model.dispatch("UPDATE_FILTER", {
                ...position,
                sheetId: this.env.model.getters.getActiveSheetId(),
                values: this.state.values.filter((val) => !val.checked).map((val) => val.string),
            });
            (_b = (_a = this.props).onClosed) === null || _b === void 0 ? void 0 : _b.call(_a);
        }
        cancel() {
            var _a, _b;
            (_b = (_a = this.props).onClosed) === null || _b === void 0 ? void 0 : _b.call(_a);
        }
        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();
                    break;
                case "ArrowUp":
                    if (selectedIndex === undefined) {
                        selectedIndex = displayedValues.length - 1;
                    }
                    else {
                        selectedIndex = Math.max(selectedIndex - 1, 0);
                    }
                    ev.preventDefault();
                    break;
                case "Enter":
                    if (selectedIndex !== undefined) {
                        this.checkValue(displayedValues[selectedIndex]);
                    }
                    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) {
            var _a, _b;
            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 },
            });
            (_b = (_a = this.props).onClosed) === null || _b === void 0 ? void 0 : _b.call(_a);
        }
    }
    FilterMenu.size = { width: MENU_WIDTH, height: FILTER_MENU_HEIGHT };
    FilterMenu.template = "o-spreadsheet-FilterMenu";
    FilterMenu.style = CSS$2;
    FilterMenu.components = { FilterMenuValueItem };
    const FilterMenuPopoverBuilder = {
        onOpen: (position, getters) => {
            return {
                isOpen: true,
                props: { filterPosition: position },
                Component: FilterMenu,
                cellCorner: "BottomLeft",
            };
        },
    };

    function getMenuChildren(node, env) {
        const children = [];
        for (const child of node.children) {
            if (typeof child === "function") {
                children.push(...child(env));
            }
            else {
                children.push(child);
            }
        }
        return children.sort((a, b) => a.sequence - b.sequence);
    }
    function getMenuName(node, env) {
        if (typeof node.name === "function") {
            return node.name(env);
        }
        return node.name;
    }
    function getMenuDescription(node) {
        return node.description ? node.description : "";
    }

    const macRegex = /Mac/i;
    const MODIFIER_KEYS = ["Shift", "Control", "Alt", "Meta"];
    /**
     * 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 === null || spreadsheetElement === void 0 ? void 0 : spreadsheetElement.getBoundingClientRect();
            return { top, left };
        }
        throw new Error("Can't find spreadsheet position");
    }
    function getOpenedMenus() {
        return Array.from(document.querySelectorAll(".o-spreadsheet .o-menu"));
    }
    const letterRegex = /^[a-zA-Z]$/;
    /**
     * Transform a keyboard event into a shortcut string that represent this event. The letters keys will be uppercased.
     *
     * @argument ev - The keyboard event to transform
     * @argument mode - Use either ev.key of ev.code to get the string shortcut
     *
     * @example
     * event : { ctrlKey: true, key: "a" } => "Ctrl+A"
     * event : { shift: true, alt: true, key: "Home" } => "Alt+Shift+Home"
     */
    function keyboardEventToShortcutString(ev, mode = "key") {
        let keyDownString = "";
        if (!MODIFIER_KEYS.includes(ev.key)) {
            if (isCtrlKey(ev))
                keyDownString += "Ctrl+";
            if (ev.altKey)
                keyDownString += "Alt+";
            if (ev.shiftKey)
                keyDownString += "Shift+";
        }
        const key = mode === "key" ? ev.key : ev.code;
        keyDownString += letterRegex.test(key) ? key.toUpperCase() : key;
        return keyDownString;
    }
    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) top left position (in pixels) 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 useAbsolutePosition(ref) {
        const position = owl.useState({ x: 0, y: 0 });
        function updateElPosition() {
            const el = ref.el;
            if (el === null) {
                return;
            }
            const { top, left } = el.getBoundingClientRect();
            if (left !== position.x || top !== position.y) {
                position.x = left;
                position.y = top;
            }
        }
        owl.onMounted(updateElPosition);
        owl.onPatched(updateElPosition);
        return position;
    }

    class Popover extends owl.Component {
        constructor() {
            super(...arguments);
            this.spreadsheetRect = useSpreadsheetRect();
        }
        get maxHeight() {
            return Math.max(0, this.viewportDimension.height - BOTTOMBAR_HEIGHT - SCROLLBAR_WIDTH);
        }
        get style() {
            // the props's position is expressed relative to the "body" element
            // but we teleport the element in ".o-spreadsheet" to keep everything
            // within our control and to avoid leaking into external DOM
            const horizontalPosition = `left:${this.horizontalPosition() - this.spreadsheetRect.x}`;
            const verticalPosition = `top:${this.verticalPosition() - this.spreadsheetRect.y}`;
            const maxHeight = this.maxHeight;
            const height = `max-height:${maxHeight}`;
            const shadow = maxHeight !== 0 ? "box-shadow: 1px 2px 5px 2px rgb(51 51 51 / 15%);" : "";
            return `
      position: absolute;
      z-index: ${this.props.zIndex};
      ${verticalPosition}px;
      ${horizontalPosition}px;
      ${height}px;
      width:${this.props.childWidth}px;
      overflow-y: auto;
      overflow-x: hidden;
      ${shadow}
    `;
        }
        get viewportDimension() {
            return this.env.model.getters.getSheetViewDimensionWithHeaders();
        }
        get shouldRenderRight() {
            const { x } = this.props.position;
            return x + this.props.childWidth < this.viewportDimension.width;
        }
        get shouldRenderBottom() {
            const { y } = this.props.position;
            return (y + Math.min(this.props.childHeight, this.maxHeight) <
                this.viewportDimension.height + (this.env.isDashboard() ? 0 : TOPBAR_HEIGHT));
        }
        horizontalPosition() {
            const { x } = this.props.position;
            if (this.shouldRenderRight) {
                return x;
            }
            return x - this.props.childWidth - this.props.flipHorizontalOffset;
        }
        verticalPosition() {
            const { y } = this.props.position;
            if (this.shouldRenderBottom) {
                return y;
            }
            return Math.max(y - this.props.childHeight + this.props.flipVerticalOffset, this.props.marginTop);
        }
    }
    Popover.template = "o-spreadsheet-Popover";
    Popover.defaultProps = {
        flipHorizontalOffset: 0,
        flipVerticalOffset: 0,
        verticalOffset: 0,
        marginTop: 0,
        onMouseWheel: () => { },
        zIndex: ComponentsImportance.Popover,
    };

    //------------------------------------------------------------------------------
    // Context Menu Component
    //------------------------------------------------------------------------------
    css /* scss */ `
  .o-menu {
    background-color: white;
    padding: 5px 0px;
    .o-menu-item {
      display: flex;
      justify-content: space-between;
      align-items: center;
      box-sizing: border-box;
      height: ${MENU_ITEM_HEIGHT}px;
      padding: 4px 16px;
      cursor: pointer;
      user-select: none;

      .o-menu-item-name {
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
      }

      &.o-menu-root {
        display: flex;
        justify-content: space-between;
      }
      .o-menu-item-icon {
        margin-top: auto;
        margin-bottom: auto;
      }
      .o-icon {
        width: 10px;
      }

      &:not(.disabled) {
        &:hover,
        &.o-menu-item-active {
          background-color: #ebebeb;
        }
        .o-menu-item-description {
          color: grey;
        }
      }
      &.disabled {
        color: ${DISABLED_TEXT_COLOR};
        cursor: not-allowed;
      }
    }
  }
`;
    class Menu extends owl.Component {
        constructor() {
            super(...arguments);
            this.MENU_WIDTH = MENU_WIDTH;
            this.subMenu = owl.useState({
                isOpen: false,
                position: null,
                scrollOffset: 0,
                menuItems: [],
            });
            this.menuRef = owl.useRef("menu");
            this.position = useAbsolutePosition(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 visibleMenuItems() {
            return this.props.menuItems.filter((x) => x.isVisible(this.env));
        }
        get subMenuPosition() {
            const position = Object.assign({}, this.subMenu.position);
            position.y -= this.subMenu.scrollOffset || 0;
            return position;
        }
        get menuHeight() {
            return this.menuComponentHeight(this.visibleMenuItems);
        }
        get subMenuHeight() {
            return this.menuComponentHeight(this.subMenu.menuItems);
        }
        get popover() {
            const isRoot = this.props.depth === 1;
            let marginTop = 6;
            if (!this.env.isDashboard()) {
                marginTop += TOPBAR_HEIGHT + HEADER_HEIGHT;
            }
            return {
                // some margin between the header and the component
                marginTop,
                flipHorizontalOffset: MENU_WIDTH * (this.props.depth - 1),
                flipVerticalOffset: isRoot ? 0 : MENU_ITEM_HEIGHT,
            };
        }
        getColor(menu) {
            return menu.textColor ? `color: ${menu.textColor}` : undefined;
        }
        async activateMenu(menu) {
            var _a, _b;
            const result = await menu.action(this.env);
            this.close();
            (_b = (_a = this.props).onMenuClicked) === null || _b === void 0 ? void 0 : _b.call(_a, { detail: result });
        }
        close() {
            this.closeSubMenu();
            this.props.onClose();
        }
        /**
         * Return the number of pixels between the top of the menu
         * and the menu item at a given index.
         */
        subMenuVerticalPosition(position) {
            const menusAbove = this.visibleMenuItems.slice(0, position);
            return this.menuComponentHeight(menusAbove) + this.position.y;
        }
        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();
        }
        /**
         * Return the total height (in pixels) needed for some
         * menu items
         */
        menuComponentHeight(menuItems) {
            const separators = menuItems.filter((m) => m.separator);
            const others = menuItems;
            return MENU_ITEM_HEIGHT * others.length + separators.length * MENU_SEPARATOR_HEIGHT;
        }
        getName(menu) {
            return getMenuName(menu, this.env);
        }
        getDescription(menu) {
            return getMenuDescription(menu);
        }
        isRoot(menu) {
            return !menu.action;
        }
        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, position) {
            const y = this.subMenuVerticalPosition(position);
            this.subMenu.position = {
                x: this.position.x + MENU_WIDTH,
                y: y - (this.subMenu.scrollOffset || 0),
            };
            this.subMenu.menuItems = getMenuChildren(menu, this.env).filter((item) => !item.isVisible || item.isVisible(this.env));
            this.subMenu.isOpen = true;
            this.subMenu.parentMenu = menu;
        }
        isParentMenu(subMenu, menuItem) {
            var _a;
            return ((_a = subMenu.parentMenu) === null || _a === void 0 ? void 0 : _a.id) === menuItem.id;
        }
        closeSubMenu() {
            this.subMenu.isOpen = false;
            this.subMenu.parentMenu = undefined;
        }
        onClickMenu(menu, position) {
            if (this.isEnabled(menu)) {
                if (this.isRoot(menu)) {
                    this.openSubMenu(menu, position);
                }
                else {
                    this.activateMenu(menu);
                }
            }
        }
        onMouseOver(menu, position) {
            if (menu.isEnabled(this.env)) {
                if (this.isRoot(menu)) {
                    this.openSubMenu(menu, position);
                }
                else {
                    this.closeSubMenu();
                }
            }
        }
    }
    Menu.template = "o-spreadsheet-Menu";
    Menu.components = { Menu, Popover };
    Menu.defaultProps = {
        depth: 1,
    };

    const LINK_TOOLTIP_HEIGHT = 43;
    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;

    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 {
        get cell() {
            const { col, row } = this.props.cellPosition;
            const sheetId = this.env.model.getters.getActiveSheetId();
            const cell = this.env.model.getters.getCell(sheetId, col, row);
            if (cell === null || cell === void 0 ? void 0 : cell.isLink()) {
                return cell;
            }
            throw new Error(`LinkDisplay Component can only be used with link cells. ${toXC(col, row)} is not a link.`);
        }
        openLink() {
            this.cell.action(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.cell.style;
            const textColor = (style === null || style === void 0 ? void 0 : style.textColor) === LINK_COLOR ? undefined : style === null || style === void 0 ? void 0 : style.textColor;
            this.env.model.dispatch("UPDATE_CELL", {
                col,
                row,
                sheetId,
                content: this.cell.link.label,
                style: { ...style, textColor, underline: undefined },
            });
        }
    }
    LinkDisplay.components = { Menu };
    LinkDisplay.template = "o-spreadsheet-LinkDisplay";
    LinkDisplay.size = { width: LINK_TOOLTIP_WIDTH, height: LINK_TOOLTIP_HEIGHT };
    const LinkCellPopoverBuilder = {
        onHover: (position, getters) => {
            const sheetId = getters.getActiveSheetId();
            const cell = getters.getCell(sheetId, position.col, position.row);
            const shouldDisplayLink = !getters.isDashboard() &&
                (cell === null || cell === void 0 ? void 0 : cell.isLink()) &&
                getters.isVisibleInViewport(sheetId, position.col, position.row);
            if (!shouldDisplayLink)
                return { isOpen: false };
            return {
                isOpen: true,
                Component: LinkDisplay,
                props: { cellPosition: position },
                cellCorner: "BottomLeft",
            };
        },
    };

    const DEFAULT_MENU_ITEM = (key) => ({
        isVisible: () => true,
        isEnabled: () => true,
        isReadonlyAllowed: false,
        description: "",
        action: false,
        children: [],
        separator: false,
        icon: false,
        id: key,
    });
    function createFullMenuItem(key, value) {
        return Object.assign({}, DEFAULT_MENU_ITEM(key), value);
    }
    function isMenuItem(value) {
        return typeof value !== "function";
    }
    /**
     * The class Registry is extended in order to add the function addChild
     *
     */
    class MenuItemRegistry extends Registry {
        /**
         * @override
         */
        add(key, value) {
            this.content[key] = createFullMenuItem(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) {
            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) {
                node = node.children.filter(isMenuItem).find((elt) => elt.id === p);
                if (!node) {
                    throw new Error(`Path ${root + ":" + path.join(":")} not found`);
                }
            }
            if (typeof value !== "function") {
                node.children.push(createFullMenuItem(key, value));
            }
            else {
                node.children.push(value);
            }
            return this;
        }
        /**
         * Get a list of all elements in the registry, ordered by sequence
         * @override
         */
        getAll() {
            return super.getAll().sort((a, b) => a.sequence - b.sequence);
        }
    }

    //------------------------------------------------------------------------------
    // Link Menu Registry
    //------------------------------------------------------------------------------
    const linkMenuRegistry = new MenuItemRegistry();
    linkMenuRegistry
        .add("sheet", {
        name: _lt("Link sheet"),
        sequence: 10,
    })
        .addChild("sheet_list", ["sheet"], (env) => {
        const sheets = env.model.getters
            .getSheetIds()
            .map((sheetId) => env.model.getters.getSheet(sheetId));
        return sheets.map((sheet, i) => createFullMenuItem(sheet.id, {
            name: sheet.name,
            sequence: i,
            action: () => ({
                link: { label: sheet.name, url: buildSheetLink(sheet.id) },
                urlRepresentation: sheet.name,
                isUrlEditable: false,
            }),
        }));
    });

    const MENU_OFFSET_X = 320;
    const MENU_OFFSET_Y = 100;
    const PADDING = 12;
    const LINK_EDITOR_WIDTH = 340;
    const LINK_EDITOR_HEIGHT = 180;
    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;
    .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;
      .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);
        }
      }
      .o-button:enabled {
        cursor: pointer;
      }
      .o-button:last-child {
        margin-right: 0px;
      }
    }
    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 {
        constructor() {
            super(...arguments);
            this.menuItems = linkMenuRegistry.getAll();
            this.state = owl.useState(this.defaultState);
            this.menu = owl.useState({
                isOpen: false,
            });
            this.linkEditorRef = owl.useRef("linkEditor");
            this.position = useAbsolutePosition(this.linkEditorRef);
            this.urlInput = owl.useRef("urlInput");
        }
        setup() {
            owl.onMounted(() => { var _a; return (_a = this.urlInput.el) === null || _a === void 0 ? void 0 : _a.focus(); });
        }
        get defaultState() {
            const { col, row } = this.props.cellPosition;
            const sheetId = this.env.model.getters.getActiveSheetId();
            const cell = this.env.model.getters.getCell(sheetId, col, row);
            if (cell === null || cell === void 0 ? void 0 : cell.isLink()) {
                return {
                    link: { url: cell.link.url, label: cell.formattedValue },
                    urlRepresentation: cell.urlRepresentation,
                    isUrlEditable: cell.isUrlEditable,
                };
            }
            return {
                link: { url: "", label: (cell === null || cell === void 0 ? void 0 : cell.formattedValue) || "" },
                isUrlEditable: true,
                urlRepresentation: "",
            };
        }
        get menuPosition() {
            return {
                x: this.position.x + MENU_OFFSET_X - PADDING - 2,
                y: this.position.y + MENU_OFFSET_Y,
            };
        }
        onSpecialLink(ev) {
            const { detail } = ev;
            this.state.link.url = detail.link.url;
            this.state.link.label = detail.link.label;
            this.state.isUrlEditable = detail.isUrlEditable;
            this.state.urlRepresentation = detail.urlRepresentation;
        }
        openMenu() {
            this.menu.isOpen = true;
        }
        removeLink() {
            this.state.link.url = "";
            this.state.urlRepresentation = "";
            this.state.isUrlEditable = true;
        }
        save() {
            var _a, _b;
            const { col, row } = this.props.cellPosition;
            const label = this.state.link.label || this.state.link.url;
            this.env.model.dispatch("UPDATE_CELL", {
                col: col,
                row: row,
                sheetId: this.env.model.getters.getActiveSheetId(),
                content: markdownLink(label, this.state.link.url),
            });
            (_b = (_a = this.props).onClosed) === null || _b === void 0 ? void 0 : _b.call(_a);
        }
        cancel() {
            var _a, _b;
            (_b = (_a = this.props).onClosed) === null || _b === void 0 ? void 0 : _b.call(_a);
        }
        onKeyDown(ev) {
            switch (ev.key) {
                case "Enter":
                    if (this.state.link.url) {
                        this.save();
                    }
                    ev.preventDefault();
                    break;
                case "Escape":
                    this.cancel();
                    break;
            }
        }
    }
    LinkEditor.template = "o-spreadsheet-LinkEditor";
    LinkEditor.size = { width: LINK_EDITOR_WIDTH, height: LINK_EDITOR_HEIGHT };
    LinkEditor.components = { Menu };
    const LinkEditorPopoverBuilder = {
        onOpen: (position, getters) => {
            return {
                isOpen: true,
                props: { cellPosition: position },
                Component: LinkEditor,
                cellCorner: "BottomLeft",
            };
        },
    };

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

    /**
     * This registry is intended to map a cell content (raw string) to
     * an instance of a cell.
     */
    const cellRegistry = new Registry();

    /**
     * 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();

    const figureRegistry = new Registry();

    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,
            },
        ];
    }

    const SORT_TYPES = [
        CellValueType.number,
        CellValueType.error,
        CellValueType.text,
        CellValueType.boolean,
    ];
    function convertCell(cell, index) {
        return {
            index,
            type: cell ? cell.evaluated.type : CellValueType.empty,
            value: cell ? cell.evaluated.value : "",
        };
    }
    function sortCells(cells, sortDirection, emptyCellAsZero) {
        const cellsWithIndex = cells.map(convertCell);
        let emptyCells = cellsWithIndex.filter((x) => x.type === CellValueType.empty);
        let nonEmptyCells = cellsWithIndex.filter((x) => x.type !== CellValueType.empty);
        if (emptyCellAsZero) {
            nonEmptyCells.push(...emptyCells.map((emptyCell) => ({ ...emptyCell, type: CellValueType.number, value: 0 })));
            emptyCells = [];
        }
        const inverse = sortDirection === "descending" ? -1 : 1;
        return nonEmptyCells
            .sort((left, right) => {
            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;
        })
            .concat(emptyCells);
    }
    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 r = zone.top; r <= zone.bottom; r++) {
                table = [];
                for (let c = zone.left; c <= zone.right; c++) {
                    let merge = env.model.getters.getMerge(sheetId, c, r);
                    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(_lt("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(64 /* CommandResult.InvalidSortZone */)) {
            const { col, row } = anchor;
            env.model.selection.selectZone({ cell: { col, row }, zone });
            env.raiseError(_lt("Cannot sort. To sort, select only cells or only merges that have the same size."));
        }
    }

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

    const AddFilterInteractiveContent = {
        filterOverlap: _lt("You cannot create overlapping filters."),
        nonContinuousTargets: _lt("A filter can only be created on a continuous selection."),
        mergeInFilter: _lt("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(79 /* CommandResult.FilterOverlap */)) {
            env.raiseError(AddFilterInteractiveContent.filterOverlap);
        }
        else if (result.isCancelledBecause(81 /* CommandResult.MergeInFilter */)) {
            env.raiseError(AddFilterInteractiveContent.mergeInFilter);
        }
        else if (result.isCancelledBecause(82 /* CommandResult.NonContinuousTargets */)) {
            env.raiseError(AddFilterInteractiveContent.nonContinuousTargets);
        }
    }

    const PasteInteractiveContent = {
        wrongPasteSelection: _lt("This operation is not allowed with multiple selections."),
        willRemoveExistingMerge: _lt("This operation is not possible due to a merge. Please remove the merges first than try again."),
        wrongFigurePasteOption: _lt("Cannot do a special paste of a figure."),
        frozenPaneOverlap: _lt("Cannot paste merged cells over a frozen pane."),
    };
    function handlePasteResult(env, result) {
        if (!result.isSuccessful) {
            if (result.reasons.includes(20 /* CommandResult.WrongPasteSelection */)) {
                env.raiseError(PasteInteractiveContent.wrongPasteSelection);
            }
            else if (result.reasons.includes(2 /* CommandResult.WillRemoveExistingMerge */)) {
                env.raiseError(PasteInteractiveContent.willRemoveExistingMerge);
            }
            else if (result.reasons.includes(22 /* CommandResult.WrongFigurePasteOption */)) {
                env.raiseError(PasteInteractiveContent.wrongFigurePasteOption);
            }
            else if (result.reasons.includes(76 /* 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) {
        const result = env.model.dispatch("PASTE_FROM_OS_CLIPBOARD", { target, text });
        handlePasteResult(env, result);
    }

    //------------------------------------------------------------------------------
    // Helpers
    //------------------------------------------------------------------------------
    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;
        }
    }
    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 setFormatter(env, format) {
        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,
        });
    }
    async function readOsClipboard(env) {
        try {
            return await env.clipboard.readText();
        }
        catch (e) {
            // Permission is required to read the clipboard.
            console.warn("The OS clipboard could not be read.");
            console.error(e);
            return undefined;
        }
    }
    //------------------------------------------------------------------------------
    // Simple actions
    //------------------------------------------------------------------------------
    const UNDO_ACTION = (env) => env.model.dispatch("REQUEST_UNDO");
    const REDO_ACTION = (env) => env.model.dispatch("REQUEST_REDO");
    const COPY_ACTION = async (env) => {
        env.model.dispatch("COPY");
        await env.clipboard.writeText(env.model.getters.getClipboardContent());
    };
    const CUT_ACTION = async (env) => {
        interactiveCut(env);
        await env.clipboard.writeText(env.model.getters.getClipboardContent());
    };
    const PASTE_ACTION = async (env) => {
        const spreadsheetClipboard = env.model.getters.getClipboardContent();
        const osClipboard = await readOsClipboard(env);
        const target = env.model.getters.getSelectedZones();
        if (osClipboard && osClipboard !== spreadsheetClipboard) {
            interactivePasteFromOS(env, target, osClipboard);
        }
        else {
            interactivePaste(env, target);
        }
    };
    const PASTE_VALUE_ACTION = async (env) => {
        const spreadsheetClipboard = env.model.getters.getClipboardContent();
        const osClipboard = await readOsClipboard(env);
        const target = env.model.getters.getSelectedZones();
        if (osClipboard && osClipboard !== spreadsheetClipboard) {
            env.model.dispatch("PASTE_FROM_OS_CLIPBOARD", {
                target,
                text: osClipboard,
            });
        }
        else {
            env.model.dispatch("PASTE", {
                target: env.model.getters.getSelectedZones(),
                pasteOption: "onlyValue",
            });
        }
    };
    const PASTE_FORMAT_ACTION = (env) => interactivePaste(env, env.model.getters.getSelectedZones(), "onlyFormat");
    const DELETE_CONTENT_ACTION = (env) => env.model.dispatch("DELETE_CONTENT", {
        sheetId: env.model.getters.getActiveSheetId(),
        target: env.model.getters.getSelectedZones(),
    });
    const SET_FORMULA_VISIBILITY_ACTION = (env) => env.model.dispatch("SET_FORMULA_VISIBILITY", { show: !env.model.getters.shouldShowFormulas() });
    const SET_GRID_LINES_VISIBILITY_ACTION = (env) => {
        const sheetId = env.model.getters.getActiveSheetId();
        env.model.dispatch("SET_GRID_LINES_VISIBILITY", {
            sheetId,
            areGridLinesVisible: !env.model.getters.getGridLinesVisibility(sheetId),
        });
    };
    const IS_NOT_CUT_OPERATION = (env) => {
        return !env.model.getters.isCutOperation();
    };
    //------------------------------------------------------------------------------
    // Grid manipulations
    //------------------------------------------------------------------------------
    const DELETE_CONTENT_ROWS_NAME = (env) => {
        if (env.model.getters.getSelectedZones().length > 1) {
            return _lt("Clear rows");
        }
        let first;
        let last;
        const activesRows = env.model.getters.getActiveRows();
        if (activesRows.size !== 0) {
            first = largeMin([...activesRows]);
            last = largeMax([...activesRows]);
        }
        else {
            const zone = env.model.getters.getSelectedZones()[0];
            first = zone.top;
            last = zone.bottom;
        }
        if (first === last) {
            return _lt("Clear row %s", (first + 1).toString());
        }
        return _lt("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 _lt("Clear columns");
        }
        let first;
        let last;
        const activeCols = env.model.getters.getActiveCols();
        if (activeCols.size !== 0) {
            first = largeMin([...activeCols]);
            last = largeMax([...activeCols]);
        }
        else {
            const zone = env.model.getters.getSelectedZones()[0];
            first = zone.left;
            last = zone.right;
        }
        if (first === last) {
            return _lt("Clear column %s", numberToLetters(first));
        }
        return _lt("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 _lt("Delete rows");
        }
        let first;
        let last;
        const activesRows = env.model.getters.getActiveRows();
        if (activesRows.size !== 0) {
            first = largeMin([...activesRows]);
            last = largeMax([...activesRows]);
        }
        else {
            const zone = env.model.getters.getSelectedZones()[0];
            first = zone.top;
            last = zone.bottom;
        }
        if (first === last) {
            return _lt("Delete row %s", (first + 1).toString());
        }
        return _lt("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) => {
        if ((dimension === "COL" && env.model.getters.getActiveRows().size > 0) ||
            (dimension === "ROW" && env.model.getters.getActiveCols().size > 0)) {
            return false;
        }
        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 _lt("Delete columns");
        }
        let first;
        let last;
        const activeCols = env.model.getters.getActiveCols();
        if (activeCols.size !== 0) {
            first = largeMin([...activeCols]);
            last = largeMax([...activeCols]);
        }
        else {
            const zone = env.model.getters.getSelectedZones()[0];
            first = zone.left;
            last = zone.right;
        }
        if (first === last) {
            return _lt("Delete column %s", numberToLetters(first));
        }
        return _lt("Delete columns %s - %s", numberToLetters(first), numberToLetters(last));
    };
    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 INSERT_CELL_SHIFT_DOWN = (env) => {
        const zone = env.model.getters.getSelectedZone();
        const result = env.model.dispatch("INSERT_CELL", { zone, shiftDimension: "ROW" });
        handlePasteResult(env, result);
    };
    const INSERT_CELL_SHIFT_RIGHT = (env) => {
        const zone = env.model.getters.getSelectedZone();
        const result = env.model.dispatch("INSERT_CELL", { zone, shiftDimension: "COL" });
        handlePasteResult(env, result);
    };
    const DELETE_CELL_SHIFT_UP = (env) => {
        const zone = env.model.getters.getSelectedZone();
        const result = env.model.dispatch("DELETE_CELL", { zone, shiftDimension: "ROW" });
        handlePasteResult(env, result);
    };
    const DELETE_CELL_SHIFT_LEFT = (env) => {
        const zone = env.model.getters.getSelectedZone();
        const result = env.model.dispatch("DELETE_CELL", { zone, shiftDimension: "COL" });
        handlePasteResult(env, result);
    };
    const MENU_INSERT_ROWS_BEFORE_NAME = (env) => {
        const number = getRowsNumber(env);
        if (number === 1) {
            return _lt("Row above");
        }
        return _lt("%s Rows above", number.toString());
    };
    const ROW_INSERT_ROWS_BEFORE_NAME = (env) => {
        const number = getRowsNumber(env);
        return number === 1 ? _lt("Insert row above") : _lt("Insert %s rows above", number.toString());
    };
    const CELL_INSERT_ROWS_BEFORE_NAME = (env) => {
        const number = getRowsNumber(env);
        if (number === 1) {
            return _lt("Insert row");
        }
        return _lt("Insert %s rows", number.toString());
    };
    const INSERT_ROWS_BEFORE_ACTION = (env) => {
        const activeRows = env.model.getters.getActiveRows();
        let row;
        let quantity;
        if (activeRows.size) {
            row = largeMin([...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 MENU_INSERT_ROWS_AFTER_NAME = (env) => {
        const number = getRowsNumber(env);
        if (number === 1) {
            return _lt("Row below");
        }
        return _lt("%s Rows below", number.toString());
    };
    const ROW_INSERT_ROWS_AFTER_NAME = (env) => {
        const number = getRowsNumber(env);
        return number === 1 ? _lt("Insert row below") : _lt("Insert %s rows below", number.toString());
    };
    const INSERT_ROWS_AFTER_ACTION = (env) => {
        const activeRows = env.model.getters.getActiveRows();
        let row;
        let quantity;
        if (activeRows.size) {
            row = largeMax([...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 MENU_INSERT_COLUMNS_BEFORE_NAME = (env) => {
        const number = getColumnsNumber(env);
        if (number === 1) {
            return _lt("Column left");
        }
        return _lt("%s Columns left", number.toString());
    };
    const COLUMN_INSERT_COLUMNS_BEFORE_NAME = (env) => {
        const number = getColumnsNumber(env);
        return number === 1
            ? _lt("Insert column left")
            : _lt("Insert %s columns left", number.toString());
    };
    const CELL_INSERT_COLUMNS_BEFORE_NAME = (env) => {
        const number = getColumnsNumber(env);
        if (number === 1) {
            return _lt("Insert column");
        }
        return _lt("Insert %s columns", number.toString());
    };
    const INSERT_COLUMNS_BEFORE_ACTION = (env) => {
        const activeCols = env.model.getters.getActiveCols();
        let column;
        let quantity;
        if (activeCols.size) {
            column = largeMin([...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 MENU_INSERT_COLUMNS_AFTER_NAME = (env) => {
        const number = getColumnsNumber(env);
        if (number === 1) {
            return _lt("Column right");
        }
        return _lt("%s Columns right", number.toString());
    };
    const COLUMN_INSERT_COLUMNS_AFTER_NAME = (env) => {
        const number = getColumnsNumber(env);
        return number === 1
            ? _lt("Insert column right")
            : _lt("Insert %s columns right", number.toString());
    };
    const INSERT_COLUMNS_AFTER_ACTION = (env) => {
        const activeCols = env.model.getters.getActiveCols();
        let column;
        let quantity;
        if (activeCols.size) {
            column = largeMax([...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 _lt("Hide column %s", numberToLetters(first).toString());
        }
        else if (last - first + 1 === cols.length) {
            return _lt("Hide columns %s - %s", numberToLetters(first).toString(), numberToLetters(last).toString());
        }
        else {
            return _lt("Hide columns");
        }
    };
    const HIDE_COLUMNS_ACTION = (env) => {
        const columns = env.model.getters.getElementsFromSelection("COL");
        env.model.dispatch("HIDE_COLUMNS_ROWS", {
            sheetId: env.model.getters.getActiveSheetId(),
            dimension: "COL",
            elements: columns,
        });
    };
    const UNHIDE_ALL_COLUMNS_ACTION = (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()),
        });
    };
    const UNHIDE_COLUMNS_ACTION = (env) => {
        const columns = env.model.getters.getElementsFromSelection("COL");
        env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
            sheetId: env.model.getters.getActiveSheetId(),
            dimension: "COL",
            elements: 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 _lt("Hide row %s", (first + 1).toString());
        }
        else if (last - first + 1 === rows.length) {
            return _lt("Hide rows %s - %s", (first + 1).toString(), (last + 1).toString());
        }
        else {
            return _lt("Hide rows");
        }
    };
    const HIDE_ROWS_ACTION = (env) => {
        const rows = env.model.getters.getElementsFromSelection("ROW");
        env.model.dispatch("HIDE_COLUMNS_ROWS", {
            sheetId: env.model.getters.getActiveSheetId(),
            dimension: "ROW",
            elements: rows,
        });
    };
    const UNHIDE_ALL_ROWS_ACTION = (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()),
        });
    };
    const UNHIDE_ROWS_ACTION = (env) => {
        const columns = env.model.getters.getElementsFromSelection("ROW");
        env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
            sheetId: env.model.getters.getActiveSheetId(),
            dimension: "ROW",
            elements: columns,
        });
    };
    //------------------------------------------------------------------------------
    // Sheets
    //------------------------------------------------------------------------------
    const CREATE_SHEET_ACTION = (env) => {
        const activeSheetId = env.model.getters.getActiveSheetId();
        const position = env.model.getters.getSheetIds().indexOf(activeSheetId) + 1;
        const sheetId = env.model.uuidGenerator.smallUuid();
        env.model.dispatch("CREATE_SHEET", { sheetId, position });
        env.model.dispatch("ACTIVATE_SHEET", { sheetIdFrom: activeSheetId, sheetIdTo: sheetId });
    };
    //------------------------------------------------------------------------------
    // Charts
    //------------------------------------------------------------------------------
    const CREATE_CHART = (env) => {
        const getters = env.model.getters;
        const zone = getters.getSelectedZone();
        let dataSetZone = zone;
        const id = env.model.uuidGenerator.smallUuid();
        let labelRange;
        if (zone.left !== zone.right) {
            dataSetZone = { ...zone, left: zone.left + 1 };
        }
        const dataSets = [zoneToXc(dataSetZone)];
        const sheetId = getters.getActiveSheetId();
        const size = { width: DEFAULT_FIGURE_WIDTH, height: DEFAULT_FIGURE_HEIGHT };
        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 - size.width) / 2),
            y: y + scrollY + Math.max(0, (height - size.height) / 2),
        }; // Position at the center of the scrollable viewport
        let title = "";
        const cells = env.model.getters.getCellsInZone(sheetId, {
            ...dataSetZone,
            bottom: dataSetZone.top,
        });
        const dataSetsHaveTitle = !!cells.find((cell) => cell && cell.evaluated.type !== CellValueType.number);
        if (dataSetsHaveTitle) {
            const texts = cells.reduce((acc, cell) => {
                const text = cell && cell.evaluated.type !== CellValueType.error && env.model.getters.getCellText(cell);
                if (text) {
                    acc.push(text);
                }
                return acc;
            }, []);
            const lastElement = texts.splice(-1)[0];
            title = texts.join(", ");
            if (lastElement) {
                title += (title ? " " + env._t("and") + " " : "") + lastElement;
            }
        }
        if (zone.left !== zone.right) {
            labelRange = zoneToXc({
                ...zone,
                right: zone.left,
                top: dataSetsHaveTitle ? zone.top + 1 : zone.top,
            });
        }
        const newLegendPos = dataSetZone.right === dataSetZone.left ? "none" : "top"; //Using the same variable as above to identify number of columns involved.
        env.model.dispatch("CREATE_CHART", {
            sheetId,
            id,
            position,
            size,
            definition: {
                title,
                dataSets,
                labelRange,
                type: "bar",
                stacked: false,
                dataSetsHaveTitle,
                verticalAxisPosition: "left",
                legendPosition: newLegendPos,
            },
        });
        env.model.dispatch("SELECT_FIGURE", { id });
        env.openSidePanel("ChartPanel");
    };
    //------------------------------------------------------------------------------
    // Style/Format
    //------------------------------------------------------------------------------
    const FORMAT_AUTOMATIC_ACTION = (env) => setFormatter(env, "");
    const FORMAT_NUMBER_ACTION = (env) => setFormatter(env, "#,##0.00");
    const FORMAT_PERCENT_ACTION = (env) => setFormatter(env, "0.00%");
    const FORMAT_CURRENCY_ACTION = (env) => setFormatter(env, "[$$]#,##0.00");
    const FORMAT_CURRENCY_ROUNDED_ACTION = (env) => setFormatter(env, "[$$]#,##0");
    const FORMAT_DATE_ACTION = (env) => setFormatter(env, "m/d/yyyy");
    const FORMAT_TIME_ACTION = (env) => setFormatter(env, "hh:mm:ss a");
    const FORMAT_DATE_TIME_ACTION = (env) => setFormatter(env, "m/d/yyyy hh:mm:ss");
    const FORMAT_DURATION_ACTION = (env) => setFormatter(env, "hhhh:mm:ss");
    const FORMAT_BOLD_ACTION = (env) => setStyle(env, { bold: !env.model.getters.getCurrentStyle().bold });
    const FORMAT_ITALIC_ACTION = (env) => setStyle(env, { italic: !env.model.getters.getCurrentStyle().italic });
    const FORMAT_STRIKETHROUGH_ACTION = (env) => setStyle(env, { strikethrough: !env.model.getters.getCurrentStyle().strikethrough });
    const FORMAT_UNDERLINE_ACTION = (env) => setStyle(env, { underline: !env.model.getters.getCurrentStyle().underline });
    const FORMAT_CLEARFORMAT_ACTION = (env) => {
        env.model.dispatch("CLEAR_FORMATTING", {
            sheetId: env.model.getters.getActiveSheetId(),
            target: env.model.getters.getSelectedZones(),
        });
    };
    //------------------------------------------------------------------------------
    // Side panel
    //------------------------------------------------------------------------------
    const OPEN_CF_SIDEPANEL_ACTION = (env) => {
        env.openSidePanel("ConditionalFormatting", { selection: env.model.getters.getSelectedZones() });
    };
    const OPEN_FAR_SIDEPANEL_ACTION = (env) => {
        env.openSidePanel("FindAndReplace", {});
    };
    const OPEN_CUSTOM_CURRENCY_SIDEPANEL_ACTION = (env) => {
        env.openSidePanel("CustomCurrency", {});
    };
    const INSERT_LINK = (env) => {
        let { col, row } = env.model.getters.getPosition();
        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.getPosition();
        const cell = env.model.getters.getCell(sheetId, col, row);
        return cell && cell.isLink() ? _lt("Edit link") : _lt("Insert link");
    };
    //------------------------------------------------------------------------------
    // Filters action
    //------------------------------------------------------------------------------
    const FILTERS_CREATE_FILTER_TABLE = (env) => {
        const sheetId = env.model.getters.getActiveSheetId();
        const selection = env.model.getters.getSelection().zones;
        interactiveAddFilter(env, sheetId, selection);
    };
    const FILTERS_REMOVE_FILTER_TABLE = (env) => {
        const sheetId = env.model.getters.getActiveSheetId();
        env.model.dispatch("REMOVE_FILTER_TABLE", {
            sheetId,
            target: env.model.getters.getSelectedZones(),
        });
    };
    const SELECTION_CONTAINS_FILTER = (env) => {
        const sheetId = env.model.getters.getActiveSheetId();
        const selectedZones = env.model.getters.getSelectedZones();
        return env.model.getters.doesZonesContainFilter(sheetId, selectedZones);
    };
    const SELECTION_IS_CONTINUOUS = (env) => {
        const selectedZones = env.model.getters.getSelectedZones();
        return areZonesContinuous(...selectedZones);
    };
    //------------------------------------------------------------------------------
    // Sorting action
    //------------------------------------------------------------------------------
    const SORT_CELLS_ASCENDING = (env) => {
        const { anchor, zones } = env.model.getters.getSelection();
        const sheetId = env.model.getters.getActiveSheetId();
        interactiveSortSelection(env, sheetId, anchor.cell, zones[0], "ascending");
    };
    const SORT_CELLS_DESCENDING = (env) => {
        const { anchor, zones } = env.model.getters.getSelection();
        const sheetId = env.model.getters.getActiveSheetId();
        interactiveSortSelection(env, sheetId, anchor.cell, zones[0], "descending");
    };
    const IS_ONLY_ONE_RANGE = (env) => {
        return env.model.getters.getSelectedZones().length === 1;
    };

    //------------------------------------------------------------------------------
    // Context Menu Registry
    //------------------------------------------------------------------------------
    const cellMenuRegistry = new MenuItemRegistry();
    cellMenuRegistry
        .add("cut", {
        name: _lt("Cut"),
        description: "Ctrl+X",
        sequence: 10,
        action: CUT_ACTION,
    })
        .add("copy", {
        name: _lt("Copy"),
        description: "Ctrl+C",
        sequence: 20,
        isReadonlyAllowed: true,
        action: COPY_ACTION,
    })
        .add("paste", {
        name: _lt("Paste"),
        description: "Ctrl+V",
        sequence: 30,
        action: PASTE_ACTION,
    })
        .add("paste_special", {
        name: _lt("Paste special"),
        sequence: 40,
        separator: true,
        isVisible: IS_NOT_CUT_OPERATION,
    })
        .addChild("paste_value_only", ["paste_special"], {
        name: _lt("Paste values only"),
        sequence: 10,
        action: PASTE_VALUE_ACTION,
    })
        .addChild("paste_format_only", ["paste_special"], {
        name: _lt("Paste format only"),
        sequence: 20,
        action: PASTE_FORMAT_ACTION,
    })
        .add("add_row_before", {
        name: CELL_INSERT_ROWS_BEFORE_NAME,
        sequence: 70,
        action: INSERT_ROWS_BEFORE_ACTION,
        isVisible: IS_ONLY_ONE_RANGE,
    })
        .add("add_column_before", {
        name: CELL_INSERT_COLUMNS_BEFORE_NAME,
        sequence: 90,
        action: INSERT_COLUMNS_BEFORE_ACTION,
        isVisible: IS_ONLY_ONE_RANGE,
    })
        .add("insert_cell", {
        name: _lt("Insert cells"),
        sequence: 100,
        isVisible: IS_ONLY_ONE_RANGE,
        separator: true,
    })
        .addChild("insert_cell_down", ["insert_cell"], {
        name: _lt("Shift down"),
        sequence: 10,
        action: INSERT_CELL_SHIFT_DOWN,
    })
        .addChild("insert_cell_right", ["insert_cell"], {
        name: _lt("Shift right"),
        sequence: 20,
        action: INSERT_CELL_SHIFT_RIGHT,
    })
        .add("delete_row", {
        name: REMOVE_ROWS_NAME,
        sequence: 110,
        action: REMOVE_ROWS_ACTION,
        isVisible: IS_ONLY_ONE_RANGE,
    })
        .add("delete_column", {
        name: REMOVE_COLUMNS_NAME,
        sequence: 120,
        action: REMOVE_COLUMNS_ACTION,
        isVisible: IS_ONLY_ONE_RANGE,
    })
        .add("delete_cell", {
        name: _lt("Delete cells"),
        sequence: 130,
        isVisible: IS_ONLY_ONE_RANGE,
    })
        .addChild("delete_cell_up", ["delete_cell"], {
        name: _lt("Shift up"),
        sequence: 10,
        action: DELETE_CELL_SHIFT_UP,
    })
        .addChild("delete_cell_down", ["delete_cell"], {
        name: _lt("Shift left"),
        sequence: 20,
        action: DELETE_CELL_SHIFT_LEFT,
    })
        .add("insert_link", {
        name: INSERT_LINK_NAME,
        separator: true,
        sequence: 150,
        action: INSERT_LINK,
    });

    const colMenuRegistry = new MenuItemRegistry();
    colMenuRegistry
        .add("cut", {
        name: _lt("Cut"),
        description: "Ctrl+X",
        sequence: 10,
        action: CUT_ACTION,
    })
        .add("copy", {
        name: _lt("Copy"),
        description: "Ctrl+C",
        sequence: 20,
        isReadonlyAllowed: true,
        action: COPY_ACTION,
    })
        .add("paste", {
        name: _lt("Paste"),
        description: "Ctrl+V",
        sequence: 30,
        action: PASTE_ACTION,
    })
        .add("paste_special", {
        name: _lt("Paste special"),
        sequence: 40,
        separator: true,
        isVisible: IS_NOT_CUT_OPERATION,
    })
        .addChild("paste_value_only", ["paste_special"], {
        name: _lt("Paste value only"),
        sequence: 10,
        action: PASTE_VALUE_ACTION,
    })
        .addChild("paste_format_only", ["paste_special"], {
        name: _lt("Paste format only"),
        sequence: 20,
        action: PASTE_FORMAT_ACTION,
    })
        .add("sort_columns", {
        name: (env) => env.model.getters.getActiveCols().size > 1 ? _lt("Sort columns") : _lt("Sort column"),
        sequence: 50,
        isVisible: IS_ONLY_ONE_RANGE,
        separator: true,
    })
        .addChild("sort_ascending", ["sort_columns"], {
        name: _lt("Ascending (A ⟶ Z)"),
        sequence: 10,
        action: SORT_CELLS_ASCENDING,
    })
        .addChild("sort_descending", ["sort_columns"], {
        name: _lt("Descending (Z ⟶ A)"),
        sequence: 20,
        action: SORT_CELLS_DESCENDING,
    })
        .add("add_column_before", {
        name: COLUMN_INSERT_COLUMNS_BEFORE_NAME,
        sequence: 70,
        action: INSERT_COLUMNS_BEFORE_ACTION,
    })
        .add("add_column_after", {
        name: COLUMN_INSERT_COLUMNS_AFTER_NAME,
        sequence: 80,
        action: INSERT_COLUMNS_AFTER_ACTION,
    })
        .add("delete_column", {
        name: REMOVE_COLUMNS_NAME,
        sequence: 90,
        action: REMOVE_COLUMNS_ACTION,
        isVisible: (env) => CAN_REMOVE_COLUMNS_ROWS("COL", env),
    })
        .add("clear_column", {
        name: DELETE_CONTENT_COLUMNS_NAME,
        sequence: 100,
        action: DELETE_CONTENT_COLUMNS_ACTION,
    })
        .add("hide_columns", {
        name: HIDE_COLUMNS_NAME,
        sequence: 85,
        action: HIDE_COLUMNS_ACTION,
        isVisible: (env) => {
            const sheetId = env.model.getters.getActiveSheetId();
            const selectedCols = env.model.getters.getElementsFromSelection("COL");
            return !env.model.getters.checkElementsIncludeAllVisibleHeaders(sheetId, "COL", selectedCols);
        },
        separator: true,
    })
        .add("unhide_columns", {
        name: _lt("Unhide columns"),
        sequence: 86,
        action: UNHIDE_COLUMNS_ACTION,
        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));
        },
        separator: true,
    })
        .add("conditional_formatting", {
        name: _lt("Conditional formatting"),
        sequence: 110,
        action: OPEN_CF_SIDEPANEL_ACTION,
    });

    const rowMenuRegistry = new MenuItemRegistry();
    rowMenuRegistry
        .add("cut", {
        name: _lt("Cut"),
        sequence: 10,
        description: "Ctrl+X",
        action: CUT_ACTION,
    })
        .add("copy", {
        name: _lt("Copy"),
        description: "Ctrl+C",
        sequence: 20,
        isReadonlyAllowed: true,
        action: COPY_ACTION,
    })
        .add("paste", {
        name: _lt("Paste"),
        description: "Ctrl+V",
        sequence: 30,
        action: PASTE_ACTION,
    })
        .add("paste_special", {
        name: _lt("Paste special"),
        sequence: 40,
        separator: true,
        isVisible: IS_NOT_CUT_OPERATION,
    })
        .addChild("paste_value_only", ["paste_special"], {
        name: _lt("Paste value only"),
        sequence: 10,
        action: PASTE_VALUE_ACTION,
    })
        .addChild("paste_format_only", ["paste_special"], {
        name: _lt("Paste format only"),
        sequence: 20,
        action: PASTE_FORMAT_ACTION,
    })
        .add("add_row_before", {
        name: ROW_INSERT_ROWS_BEFORE_NAME,
        sequence: 50,
        action: INSERT_ROWS_BEFORE_ACTION,
    })
        .add("add_row_after", {
        name: ROW_INSERT_ROWS_AFTER_NAME,
        sequence: 60,
        action: INSERT_ROWS_AFTER_ACTION,
    })
        .add("delete_row", {
        name: REMOVE_ROWS_NAME,
        sequence: 70,
        action: REMOVE_ROWS_ACTION,
        isVisible: (env) => CAN_REMOVE_COLUMNS_ROWS("ROW", env),
    })
        .add("clear_row", {
        name: DELETE_CONTENT_ROWS_NAME,
        sequence: 80,
        action: DELETE_CONTENT_ROWS_ACTION,
    })
        .add("hide_rows", {
        name: HIDE_ROWS_NAME,
        sequence: 85,
        action: HIDE_ROWS_ACTION,
        isVisible: (env) => {
            const sheetId = env.model.getters.getActiveSheetId();
            const selectedRows = env.model.getters.getElementsFromSelection("ROW");
            return !env.model.getters.checkElementsIncludeAllVisibleHeaders(sheetId, "ROW", selectedRows);
        },
        separator: true,
    })
        .add("unhide_rows", {
        name: _lt("Unhide rows"),
        sequence: 86,
        action: UNHIDE_ROWS_ACTION,
        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));
        },
        separator: true,
    })
        .add("conditional_formatting", {
        name: _lt("Conditional formatting"),
        sequence: 90,
        action: OPEN_CF_SIDEPANEL_ACTION,
    });

    function interactiveRenameSheet(env, sheetId, errorText) {
        if (env.model.getters.isReadonly()) {
            return;
        }
        const placeholder = env.model.getters.getSheetName(sheetId);
        const title = _lt("Rename Sheet");
        const callback = (name) => {
            if (name === null || name === placeholder) {
                return;
            }
            const result = env.model.dispatch("RENAME_SHEET", { sheetId, name });
            if (!result.isSuccessful) {
                if (result.reasons.includes(10 /* CommandResult.MissingSheetName */)) {
                    interactiveRenameSheet(env, sheetId, _lt("The sheet name cannot be empty."));
                }
                if (result.reasons.includes(11 /* CommandResult.DuplicatedSheetName */)) {
                    interactiveRenameSheet(env, sheetId, _lt("A sheet with the name %s already exists. Please select another name.", name));
                }
                if (result.reasons.includes(13 /* CommandResult.ForbiddenCharactersInSheetName */)) {
                    interactiveRenameSheet(env, sheetId, _lt("Some used characters are not allowed in a sheet name (Forbidden characters are %s).", FORBIDDEN_SHEET_CHARS.join(" ")));
                }
            }
        };
        env.editText(title, callback, {
            placeholder: placeholder,
            error: errorText,
        });
    }

    const sheetMenuRegistry = new MenuItemRegistry();
    sheetMenuRegistry
        .add("delete", {
        name: _lt("Delete"),
        sequence: 10,
        isVisible: (env) => {
            return env.model.getters.getVisibleSheetIds().length > 1;
        },
        action: (env) => env.askConfirmation(_lt("Are you sure you want to delete this sheet ?"), () => {
            env.model.dispatch("DELETE_SHEET", { sheetId: env.model.getters.getActiveSheetId() });
        }),
    })
        .add("duplicate", {
        name: _lt("Duplicate"),
        sequence: 20,
        action: (env) => {
            const sheetIdFrom = env.model.getters.getActiveSheetId();
            const sheetNameFrom = env.model.getters.getSheetName(sheetIdFrom);
            const sheetIdTo = env.model.uuidGenerator.smallUuid();
            const sheetNameTo = env.model.getters.getDuplicateSheetName(sheetNameFrom);
            env.model.dispatch("DUPLICATE_SHEET", {
                sheetId: sheetIdFrom,
                sheetIdTo,
                sheetNameTo,
            });
            env.model.dispatch("ACTIVATE_SHEET", { sheetIdFrom, sheetIdTo });
        },
    })
        .add("rename", {
        name: _lt("Rename"),
        sequence: 30,
        action: (env) => interactiveRenameSheet(env, env.model.getters.getActiveSheetId()),
    })
        .add("move_right", {
        name: _lt("Move right"),
        sequence: 40,
        isVisible: (env) => {
            const sheetId = env.model.getters.getActiveSheetId();
            const sheetIds = env.model.getters.getVisibleSheetIds();
            return sheetIds.indexOf(sheetId) !== sheetIds.length - 1;
        },
        action: (env) => env.model.dispatch("MOVE_SHEET", {
            sheetId: env.model.getters.getActiveSheetId(),
            direction: "right",
        }),
    })
        .add("move_left", {
        name: _lt("Move left"),
        sequence: 50,
        isVisible: (env) => {
            const sheetId = env.model.getters.getActiveSheetId();
            return env.model.getters.getVisibleSheetIds()[0] !== sheetId;
        },
        action: (env) => env.model.dispatch("MOVE_SHEET", {
            sheetId: env.model.getters.getActiveSheetId(),
            direction: "left",
        }),
    })
        .add("hide_sheet", {
        name: _lt("Hide sheet"),
        sequence: 60,
        isVisible: (env) => env.model.getters.getVisibleSheetIds().length !== 1,
        action: (env) => env.model.dispatch("HIDE_SHEET", { sheetId: env.model.getters.getActiveSheetId() }),
    });

    const CfTerms = {
        Errors: {
            [25 /* CommandResult.InvalidRange */]: _lt("The range is invalid"),
            [52 /* CommandResult.FirstArgMissing */]: _lt("The argument is missing. Please provide a value"),
            [53 /* CommandResult.SecondArgMissing */]: _lt("The second argument is missing. Please provide a value"),
            [54 /* CommandResult.MinNaN */]: _lt("The minpoint must be a number"),
            [55 /* CommandResult.MidNaN */]: _lt("The midpoint must be a number"),
            [56 /* CommandResult.MaxNaN */]: _lt("The maxpoint must be a number"),
            [57 /* CommandResult.ValueUpperInflectionNaN */]: _lt("The first value must be a number"),
            [58 /* CommandResult.ValueLowerInflectionNaN */]: _lt("The second value must be a number"),
            [48 /* CommandResult.MinBiggerThanMax */]: _lt("Minimum must be smaller then Maximum"),
            [51 /* CommandResult.MinBiggerThanMid */]: _lt("Minimum must be smaller then Midpoint"),
            [50 /* CommandResult.MidBiggerThanMax */]: _lt("Midpoint must be smaller then Maximum"),
            [49 /* CommandResult.LowerBiggerThanUpper */]: _lt("Lower inflection point must be smaller than upper inflection point"),
            [59 /* CommandResult.MinInvalidFormula */]: _lt("Invalid Minpoint formula"),
            [61 /* CommandResult.MaxInvalidFormula */]: _lt("Invalid Maxpoint formula"),
            [60 /* CommandResult.MidInvalidFormula */]: _lt("Invalid Midpoint formula"),
            [62 /* CommandResult.ValueUpperInvalidFormula */]: _lt("Invalid upper inflection point formula"),
            [63 /* CommandResult.ValueLowerInvalidFormula */]: _lt("Invalid lower inflection point formula"),
            [24 /* CommandResult.EmptyRange */]: _lt("A range needs to be defined"),
            Unexpected: _lt("The rule is invalid for an unknown reason"),
        },
        ColorScale: _lt("Color scale"),
        IconSet: _lt("Icon set"),
    };
    const CellIsOperators = {
        IsEmpty: _lt("Is empty"),
        IsNotEmpty: _lt("Is not empty"),
        ContainsText: _lt("Contains"),
        NotContains: _lt("Does not contain"),
        BeginsWith: _lt("Starts with"),
        EndsWith: _lt("Ends with"),
        Equal: _lt("Is equal to"),
        NotEqual: _lt("Is not equal to"),
        GreaterThan: _lt("Is greater than"),
        GreaterThanOrEqual: _lt("Is greater than or equal to"),
        LessThan: _lt("Is less than"),
        LessThanOrEqual: _lt("Is less than or equal to"),
        Between: _lt("Is between"),
        NotBetween: _lt("Is not between"),
    };
    const ChartTerms = {
        Series: _lt("Series"),
        Errors: {
            Unexpected: _lt("The chart definition is invalid for an unknown reason"),
            // BASIC CHART ERRORS (LINE | BAR | PIE)
            [33 /* CommandResult.InvalidDataSet */]: _lt("The dataset is invalid"),
            [34 /* CommandResult.InvalidLabelRange */]: _lt("Labels are invalid"),
            // SCORECARD CHART ERRORS
            [35 /* CommandResult.InvalidScorecardKeyValue */]: _lt("The key value is invalid"),
            [36 /* CommandResult.InvalidScorecardBaseline */]: _lt("The baseline value is invalid"),
            // GAUGE CHART ERRORS
            [37 /* CommandResult.InvalidGaugeDataRange */]: _lt("The data range is invalid"),
            [38 /* CommandResult.EmptyGaugeRangeMin */]: _lt("A minimum range limit value is needed"),
            [39 /* CommandResult.GaugeRangeMinNaN */]: _lt("The minimum range limit value must be a number"),
            [40 /* CommandResult.EmptyGaugeRangeMax */]: _lt("A maximum range limit value is needed"),
            [41 /* CommandResult.GaugeRangeMaxNaN */]: _lt("The maximum range limit value must be a number"),
            [42 /* CommandResult.GaugeRangeMinBiggerThanRangeMax */]: _lt("Minimum range limit must be smaller than maximum range limit"),
            [43 /* CommandResult.GaugeLowerInflectionPointNaN */]: _lt("The lower inflection point value must be a number"),
            [44 /* CommandResult.GaugeUpperInflectionPointNaN */]: _lt("The upper inflection point value must be a number"),
        },
    };
    const NumberFormatTerms = {
        Automatic: _lt("Automatic"),
        Number: _lt("Number"),
        Percent: _lt("Percent"),
        Currency: _lt("Currency"),
        CurrencyRounded: _lt("Currency rounded"),
        Date: _lt("Date"),
        Time: _lt("Time"),
        DateTime: _lt("Date time"),
        Duration: _lt("Duration"),
        CustomCurrency: _lt("Custom currency"),
    };
    const CustomCurrencyTerms = {
        Custom: _lt("Custom"),
    };
    const MergeErrorMessage = _lt("Merged cells are preventing this operation. Unmerge those cells and try again.");

    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(66 /* CommandResult.MergeOverlap */)) {
            env.raiseError(MergeErrorMessage);
        }
    }

    const topbarMenuRegistry = new MenuItemRegistry();
    topbarMenuRegistry
        .add("file", { name: _lt("File"), sequence: 10 })
        .add("edit", { name: _lt("Edit"), sequence: 20 })
        .add("view", { name: _lt("View"), sequence: 30 })
        .add("insert", { name: _lt("Insert"), sequence: 40 })
        .add("format", { name: _lt("Format"), sequence: 50 })
        .add("data", { name: _lt("Data"), sequence: 60 })
        .addChild("save", ["file"], {
        name: _lt("Save"),
        description: "Ctrl+S",
        sequence: 10,
        action: () => console.log("Not implemented"),
    })
        .addChild("undo", ["edit"], {
        name: _lt("Undo"),
        description: "Ctrl+Z",
        sequence: 10,
        action: UNDO_ACTION,
    })
        .addChild("redo", ["edit"], {
        name: _lt("Redo"),
        description: "Ctrl+Y",
        sequence: 20,
        action: REDO_ACTION,
        separator: true,
    })
        .addChild("copy", ["edit"], {
        name: _lt("Copy"),
        description: "Ctrl+C",
        sequence: 30,
        isReadonlyAllowed: true,
        action: COPY_ACTION,
    })
        .addChild("cut", ["edit"], {
        name: _lt("Cut"),
        description: "Ctrl+X",
        sequence: 40,
        action: CUT_ACTION,
    })
        .addChild("paste", ["edit"], {
        name: _lt("Paste"),
        description: "Ctrl+V",
        sequence: 50,
        action: PASTE_ACTION,
    })
        .addChild("paste_special", ["edit"], {
        name: _lt("Paste special"),
        sequence: 60,
        separator: true,
        isVisible: IS_NOT_CUT_OPERATION,
    })
        .addChild("paste_special_value", ["edit", "paste_special"], {
        name: _lt("Paste value only"),
        sequence: 10,
        action: PASTE_VALUE_ACTION,
    })
        .addChild("paste_special_format", ["edit", "paste_special"], {
        name: _lt("Paste format only"),
        sequence: 20,
        action: PASTE_FORMAT_ACTION,
    })
        .addChild("sort_range", ["data"], {
        name: _lt("Sort range"),
        sequence: 62,
        isVisible: IS_ONLY_ONE_RANGE,
        separator: true,
    })
        .addChild("sort_ascending", ["data", "sort_range"], {
        name: _lt("Ascending (A ⟶ Z)"),
        sequence: 10,
        action: SORT_CELLS_ASCENDING,
    })
        .addChild("sort_descending", ["data", "sort_range"], {
        name: _lt("Descending (Z ⟶ A)"),
        sequence: 20,
        action: SORT_CELLS_DESCENDING,
    })
        .addChild("find_and_replace", ["edit"], {
        name: _lt("Find and replace"),
        description: "Ctrl+H",
        sequence: 65,
        isReadonlyAllowed: true,
        action: OPEN_FAR_SIDEPANEL_ACTION,
        separator: true,
    })
        .addChild("edit_delete_cell_values", ["edit"], {
        name: _lt("Delete values"),
        sequence: 70,
        action: DELETE_CONTENT_ACTION,
    })
        .addChild("edit_delete_row", ["edit"], {
        name: REMOVE_ROWS_NAME,
        sequence: 80,
        action: REMOVE_ROWS_ACTION,
        isVisible: (env) => CAN_REMOVE_COLUMNS_ROWS("ROW", env),
    })
        .addChild("edit_delete_column", ["edit"], {
        name: REMOVE_COLUMNS_NAME,
        sequence: 90,
        action: REMOVE_COLUMNS_ACTION,
        isVisible: (env) => CAN_REMOVE_COLUMNS_ROWS("COL", env),
    })
        .addChild("edit_delete_cell_shift_up", ["edit"], {
        name: _lt("Delete cell and shift up"),
        sequence: 93,
        action: DELETE_CELL_SHIFT_UP,
    })
        .addChild("edit_delete_cell_shift_left", ["edit"], {
        name: _lt("Delete cell and shift left"),
        sequence: 97,
        action: DELETE_CELL_SHIFT_LEFT,
    })
        .addChild("edit_unhide_columns", ["edit"], {
        name: _lt("Unhide all columns"),
        sequence: 100,
        action: UNHIDE_ALL_COLUMNS_ACTION,
        isVisible: (env) => env.model.getters.getHiddenColsGroups(env.model.getters.getActiveSheetId()).length > 0,
    })
        .addChild("edit_unhide_rows", ["edit"], {
        name: _lt("Unhide all rows"),
        sequence: 100,
        action: UNHIDE_ALL_ROWS_ACTION,
        isVisible: (env) => env.model.getters.getHiddenRowsGroups(env.model.getters.getActiveSheetId()).length > 0,
    })
        .addChild("insert_row_before", ["insert"], {
        name: MENU_INSERT_ROWS_BEFORE_NAME,
        sequence: 10,
        action: INSERT_ROWS_BEFORE_ACTION,
        isVisible: (env) => env.model.getters.getActiveCols().size === 0,
    })
        .addChild("insert_row_after", ["insert"], {
        name: MENU_INSERT_ROWS_AFTER_NAME,
        sequence: 20,
        action: INSERT_ROWS_AFTER_ACTION,
        isVisible: (env) => env.model.getters.getActiveCols().size === 0,
        separator: true,
    })
        .addChild("insert_column_before", ["insert"], {
        name: MENU_INSERT_COLUMNS_BEFORE_NAME,
        sequence: 30,
        action: INSERT_COLUMNS_BEFORE_ACTION,
        isVisible: (env) => env.model.getters.getActiveRows().size === 0,
    })
        .addChild("insert_column_after", ["insert"], {
        name: MENU_INSERT_COLUMNS_AFTER_NAME,
        sequence: 40,
        action: INSERT_COLUMNS_AFTER_ACTION,
        isVisible: (env) => env.model.getters.getActiveRows().size === 0,
        separator: true,
    })
        .addChild("insert_insert_cell_shift_down", ["insert"], {
        name: _lt("Insert cells and shift down"),
        sequence: 43,
        action: INSERT_CELL_SHIFT_DOWN,
    })
        .addChild("insert_insert_cell_shift_right", ["insert"], {
        name: _lt("Insert cells and shift right"),
        sequence: 47,
        action: INSERT_CELL_SHIFT_RIGHT,
        separator: true,
    })
        .addChild("insert_chart", ["insert"], {
        name: _lt("Chart"),
        sequence: 50,
        action: CREATE_CHART,
    })
        .addChild("insert_link", ["insert"], {
        name: _lt("Link"),
        separator: true,
        sequence: 60,
        action: INSERT_LINK,
    })
        .addChild("insert_sheet", ["insert"], {
        name: _lt("New sheet"),
        sequence: 70,
        action: CREATE_SHEET_ACTION,
        separator: true,
    })
        .addChild("unfreeze_panes", ["view"], {
        name: _lt("Unfreeze"),
        sequence: 4,
        isVisible: (env) => {
            const { xSplit, ySplit } = env.model.getters.getPaneDivisions(env.model.getters.getActiveSheetId());
            return xSplit + ySplit > 0;
        },
        action: (env) => env.model.dispatch("UNFREEZE_COLUMNS_ROWS", {
            sheetId: env.model.getters.getActiveSheetId(),
        }),
    })
        .addChild("freeze_panes", ["view"], {
        name: _lt("Freeze"),
        sequence: 5,
        separator: true,
    })
        .addChild("unfreeze_rows", ["view", "freeze_panes"], {
        name: _lt("No rows"),
        action: (env) => env.model.dispatch("UNFREEZE_ROWS", {
            sheetId: env.model.getters.getActiveSheetId(),
        }),
        isReadonlyAllowed: true,
        sequence: 5,
        isVisible: (env) => !!env.model.getters.getPaneDivisions(env.model.getters.getActiveSheetId()).ySplit,
    })
        .addChild("freeze_first_row", ["view", "freeze_panes"], {
        name: _lt("1 row"),
        action: (env) => interactiveFreezeColumnsRows(env, "ROW", 1),
        isReadonlyAllowed: true,
        sequence: 10,
    })
        .addChild("freeze_second_row", ["view", "freeze_panes"], {
        name: _lt("2 rows"),
        action: (env) => interactiveFreezeColumnsRows(env, "ROW", 2),
        isReadonlyAllowed: true,
        sequence: 15,
    })
        .addChild("freeze_current_row", ["view", "freeze_panes"], {
        name: _lt("Up to current row"),
        action: (env) => {
            const { bottom } = env.model.getters.getSelectedZone();
            interactiveFreezeColumnsRows(env, "ROW", bottom + 1);
        },
        isReadonlyAllowed: true,
        sequence: 20,
        separator: true,
    })
        .addChild("unfreeze_columns", ["view", "freeze_panes"], {
        name: _lt("No columns"),
        action: (env) => env.model.dispatch("UNFREEZE_COLUMNS", {
            sheetId: env.model.getters.getActiveSheetId(),
        }),
        isReadonlyAllowed: true,
        sequence: 25,
        isVisible: (env) => !!env.model.getters.getPaneDivisions(env.model.getters.getActiveSheetId()).xSplit,
    })
        .addChild("freeze_first_col", ["view", "freeze_panes"], {
        name: _lt("1 column"),
        action: (env) => interactiveFreezeColumnsRows(env, "COL", 1),
        isReadonlyAllowed: true,
        sequence: 30,
    })
        .addChild("freeze_second_col", ["view", "freeze_panes"], {
        name: _lt("2 columns"),
        action: (env) => interactiveFreezeColumnsRows(env, "COL", 2),
        isReadonlyAllowed: true,
        sequence: 35,
    })
        .addChild("freeze_current_col", ["view", "freeze_panes"], {
        name: _lt("Up to current column"),
        action: (env) => {
            const { right } = env.model.getters.getSelectedZone();
            interactiveFreezeColumnsRows(env, "COL", right + 1);
        },
        isReadonlyAllowed: true,
        sequence: 40,
    })
        .addChild("view_gridlines", ["view"], {
        name: (env) => env.model.getters.getGridLinesVisibility(env.model.getters.getActiveSheetId())
            ? _lt("Hide gridlines")
            : _lt("Show gridlines"),
        action: SET_GRID_LINES_VISIBILITY_ACTION,
        sequence: 10,
    })
        .addChild("view_formulas", ["view"], {
        name: (env) => env.model.getters.shouldShowFormulas() ? _lt("Hide formulas") : _lt("Show formulas"),
        action: SET_FORMULA_VISIBILITY_ACTION,
        isReadonlyAllowed: true,
        sequence: 15,
    })
        .addChild("format_number", ["format"], {
        name: _lt("Numbers"),
        sequence: 10,
        separator: true,
    })
        .addChild("format_number_automatic", ["format", "format_number"], {
        name: NumberFormatTerms.Automatic,
        sequence: 10,
        separator: true,
        action: FORMAT_AUTOMATIC_ACTION,
    })
        .addChild("format_number_number", ["format", "format_number"], {
        name: NumberFormatTerms.Number,
        description: "1,000.12",
        sequence: 20,
        action: FORMAT_NUMBER_ACTION,
    })
        .addChild("format_number_percent", ["format", "format_number"], {
        name: NumberFormatTerms.Percent,
        description: "10.12%",
        sequence: 30,
        separator: true,
        action: FORMAT_PERCENT_ACTION,
    })
        .addChild("format_number_currency", ["format", "format_number"], {
        name: NumberFormatTerms.Currency,
        description: "$1,000.12",
        sequence: 37,
        action: FORMAT_CURRENCY_ACTION,
    })
        .addChild("format_number_currency_rounded", ["format", "format_number"], {
        name: NumberFormatTerms.CurrencyRounded,
        description: "$1,000",
        sequence: 38,
        action: FORMAT_CURRENCY_ROUNDED_ACTION,
    })
        .addChild("format_custom_currency", ["format", "format_number"], {
        name: NumberFormatTerms.CustomCurrency,
        sequence: 39,
        separator: true,
        action: OPEN_CUSTOM_CURRENCY_SIDEPANEL_ACTION,
    })
        .addChild("format_number_date", ["format", "format_number"], {
        name: NumberFormatTerms.Date,
        description: "9/26/2008",
        sequence: 40,
        action: FORMAT_DATE_ACTION,
    })
        .addChild("format_number_time", ["format", "format_number"], {
        name: NumberFormatTerms.Time,
        description: "10:43:00 PM",
        sequence: 50,
        action: FORMAT_TIME_ACTION,
    })
        .addChild("format_number_date_time", ["format", "format_number"], {
        name: NumberFormatTerms.DateTime,
        description: "9/26/2008 22:43:00",
        sequence: 60,
        action: FORMAT_DATE_TIME_ACTION,
    })
        .addChild("format_number_duration", ["format", "format_number"], {
        name: NumberFormatTerms.Duration,
        description: "27:51:38",
        sequence: 70,
        separator: true,
        action: FORMAT_DURATION_ACTION,
    })
        .addChild("format_bold", ["format"], {
        name: _lt("Bold"),
        sequence: 20,
        description: "Ctrl+B",
        action: FORMAT_BOLD_ACTION,
    })
        .addChild("format_italic", ["format"], {
        name: _lt("Italic"),
        sequence: 30,
        description: "Ctrl+I",
        action: FORMAT_ITALIC_ACTION,
    })
        .addChild("format_underline", ["format"], {
        name: _lt("Underline"),
        description: "Ctrl+U",
        sequence: 40,
        action: FORMAT_UNDERLINE_ACTION,
    })
        .addChild("format_strikethrough", ["format"], {
        name: _lt("Strikethrough"),
        sequence: 50,
        action: FORMAT_STRIKETHROUGH_ACTION,
        separator: true,
    })
        .addChild("format_font_size", ["format"], {
        name: _lt("Font size"),
        sequence: 60,
    })
        .addChild("format_wrapping", ["format"], {
        name: _lt("Wrapping"),
        sequence: 70,
        separator: true,
    })
        .addChild("format_wrapping_overflow", ["format", "format_wrapping"], {
        name: _lt("Overflow"),
        sequence: 10,
        action: (env) => setStyle(env, { wrapping: "overflow" }),
    })
        .addChild("format_wrapping_wrap", ["format", "format_wrapping"], {
        name: _lt("Wrap"),
        sequence: 20,
        action: (env) => setStyle(env, { wrapping: "wrap" }),
    })
        .addChild("format_wrapping_clip", ["format", "format_wrapping"], {
        name: _lt("Clip"),
        sequence: 30,
        action: (env) => setStyle(env, { wrapping: "clip" }),
    })
        .addChild("format_cf", ["format"], {
        name: _lt("Conditional formatting"),
        sequence: 80,
        action: OPEN_CF_SIDEPANEL_ACTION,
        separator: true,
    })
        .addChild("format_clearFormat", ["format"], {
        name: _lt("Clear formatting"),
        sequence: 90,
        action: FORMAT_CLEARFORMAT_ACTION,
        separator: true,
    })
        .addChild("add_data_filter", ["data"], {
        name: _lt("Add Filter"),
        sequence: 20,
        action: FILTERS_CREATE_FILTER_TABLE,
        isVisible: (env) => !SELECTION_CONTAINS_FILTER(env),
        isEnabled: (env) => SELECTION_IS_CONTINUOUS(env),
    })
        .addChild("remove_data_filter", ["data"], {
        name: _lt("Remove Filter"),
        sequence: 20,
        action: FILTERS_REMOVE_FILTER_TABLE,
        isVisible: SELECTION_CONTAINS_FILTER,
    });
    // Font-sizes
    for (let fs of fontSizes) {
        topbarMenuRegistry.addChild(`format_font_size_${fs.pt}`, ["format", "format_font_size"], {
            name: fs.pt.toString(),
            sequence: fs.pt,
            action: (env) => setStyle(env, { fontSize: fs.pt }),
        });
    }

    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) {
            if (!this.content[executed]) {
                this.content[executed] = new Map();
            }
            for (const toTransform of toTransforms) {
                this.content[executed].set(toTransform, fn);
            }
            return this;
        }
        /**
         * Get the transformation function to transform the command toTransform, after
         * that the executed command happened.
         */
        getTransformation(toTransform, executed) {
            return this.content[executed] && this.content[executed].get(toTransform);
        }
    }
    const otRegistry = new OTRegistry();

    const uuidGenerator$1 = new UuidGenerator();
    css /* scss */ `
  .o-selection {
    .o-selection-input {
      display: flex;
      flex-direction: row;

      input {
        padding: 4px 6px;
        border-radius: 4px;
        box-sizing: border-box;
        flex-grow: 2;
      }
      input:focus {
        outline: none;
      }
      input.o-required,
      input.o-focused {
        border-width: 2px;
        padding: 3px 5px;
      }
      input.o-focused {
        border-color: ${SELECTION_BORDER_COLOR};
      }
      input.o-invalid {
        border-color: red;
      }
      button.o-btn {
        background: transparent;
        border: none;
        color: #333;
        cursor: pointer;
      }
      button.o-btn-action {
        margin: 8px 1px;
        border-radius: 4px;
        background: transparent;
        border: 1px solid #dadce0;
        color: #188038;
        font-weight: bold;
        font-size: 14px;
        height: 25px;
      }
    }
    /** Make the character a bit bigger
    compared to its neighbor INPUT box  */
    .o-remove-selection {
      font-weight: bold;
      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 {
        constructor() {
            super(...arguments);
            this.id = uuidGenerator$1.smallUuid();
            this.previousRanges = this.props.ranges || [];
            this.originSheet = this.env.model.getters.getActiveSheetId();
            this.state = owl.useState({
                isMissing: false,
            });
        }
        get ranges() {
            const existingSelectionRange = this.env.model.getters.getSelectionInput(this.id);
            const ranges = existingSelectionRange.length
                ? existingSelectionRange
                : this.props.ranges
                    ? this.props.ranges.map((xc, i) => ({
                        xc,
                        id: i.toString(),
                        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;
        }
        setup() {
            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);
            if (this.previousRanges.join() !== value.join()) {
                this.triggerChange();
            }
        }
        getColor(range) {
            const color = range.color || "#000";
            return "color: " + color + ";";
        }
        triggerChange() {
            var _a, _b;
            const ranges = this.env.model.getters.getSelectionInputValue(this.id);
            (_b = (_a = this.props).onSelectionChanged) === null || _b === void 0 ? void 0 : _b.call(_a, ranges);
            this.previousRanges = ranges;
        }
        extractRanges(value) {
            return this.props.hasSingleRange ? value.split(",")[0] : value;
        }
        focus(rangeId) {
            this.state.isMissing = false;
            this.env.model.dispatch("FOCUS_RANGE", {
                id: this.id,
                rangeId,
            });
        }
        addEmptyInput() {
            this.env.model.dispatch("ADD_EMPTY_RANGE", { id: this.id });
        }
        removeInput(rangeId) {
            var _a, _b;
            this.env.model.dispatch("REMOVE_RANGE", { id: this.id, rangeId });
            this.triggerChange();
            (_b = (_a = this.props).onSelectionConfirmed) === null || _b === void 0 ? void 0 : _b.call(_a);
        }
        onInputChanged(rangeId, ev) {
            const target = ev.target;
            const value = this.extractRanges(target.value);
            this.env.model.dispatch("CHANGE_RANGE", {
                id: this.id,
                rangeId,
                value,
            });
            target.blur();
            this.triggerChange();
        }
        confirm() {
            var _a, _b;
            this.env.model.dispatch("UNFOCUS_SELECTION_INPUT");
            const ranges = this.env.model.getters.getSelectionInputValue(this.id);
            if (this.props.required && ranges.length === 0) {
                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,
                });
            }
            (_b = (_a = this.props).onSelectionConfirmed) === null || _b === void 0 ? void 0 : _b.call(_a);
        }
    }
    SelectionInput.template = "o-spreadsheet-SelectionInput";

    class LineBarPieConfigPanel extends owl.Component {
        constructor() {
            super(...arguments);
            this.state = owl.useState({
                datasetDispatchResult: undefined,
                labelsDispatchResult: undefined,
            });
            this.dataSeriesRanges = [];
        }
        setup() {
            this.dataSeriesRanges = this.props.definition.dataSets;
            this.labelRange = this.props.definition.labelRange;
        }
        get errorMessages() {
            var _a, _b;
            const cancelledReasons = [
                ...(((_a = this.state.datasetDispatchResult) === null || _a === void 0 ? void 0 : _a.reasons) || []),
                ...(((_b = this.state.labelsDispatchResult) === null || _b === void 0 ? void 0 : _b.reasons) || []),
            ];
            return cancelledReasons.map((error) => ChartTerms.Errors[error] || ChartTerms.Errors.Unexpected);
        }
        get isDatasetInvalid() {
            var _a;
            return !!((_a = this.state.datasetDispatchResult) === null || _a === void 0 ? void 0 : _a.isCancelledBecause(33 /* CommandResult.InvalidDataSet */));
        }
        get isLabelInvalid() {
            var _a;
            return !!((_a = this.state.labelsDispatchResult) === null || _a === void 0 ? void 0 : _a.isCancelledBecause(34 /* 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;
        }
        onDataSeriesConfirmed() {
            this.state.datasetDispatchResult = this.props.updateChart(this.props.figureId, {
                dataSets: this.dataSeriesRanges,
            });
        }
        /**
         * Change the local labelRange. The model should be updated when the
         * button "confirm" is clicked
         */
        onLabelRangeChanged(ranges) {
            this.labelRange = ranges[0];
        }
        onLabelRangeConfirmed() {
            this.state.labelsDispatchResult = this.props.updateChart(this.props.figureId, {
                labelRange: this.labelRange,
            });
        }
    }
    LineBarPieConfigPanel.template = "o-spreadsheet-LineBarPieConfigPanel";
    LineBarPieConfigPanel.components = { SelectionInput };

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

    /**
     * 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 {
        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");
        }
    }

    /**
     * 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;
    }

    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;
    }

    /**
     * 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) {
        var _a;
        const labelZone = (_a = ds.labelCell) === null || _a === void 0 ? void 0 : _a.zone;
        let dataZone = ds.dataRange.zone;
        if (labelZone) {
            const { height, width } = zoneToDimension(dataZone);
            if (height === 1) {
                dataZone = { ...dataZone, left: dataZone.left + 1 };
            }
            else if (width === 1) {
                dataZone = { ...dataZone, top: dataZone.top + 1 };
            }
        }
        const dataRange = ds.dataRange.clone({ zone: dataZone });
        return {
            label: ds.labelCell
                ? getters.getRangeString(ds.labelCell, "forceSheetReference", true)
                : undefined,
            range: getters.getRangeString(dataRange, "forceSheetReference", 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 {
        constructor() {
            this.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 33 /* CommandResult.InvalidDataSet */;
            }
            const zones = definition.dataSets.map(toUnboundedZone);
            if (zones.some((zone) => zone.top !== zone.bottom && isFullRow(zone))) {
                return 33 /* CommandResult.InvalidDataSet */;
            }
        }
        return 0 /* CommandResult.Success */;
    }
    function checkLabelRange(definition) {
        if (definition.labelRange) {
            const invalidLabels = !rangeReference.test(definition.labelRange || "");
            if (invalidLabels) {
                return 34 /* CommandResult.InvalidLabelRange */;
            }
        }
        return 0 /* CommandResult.Success */;
    }
    // ---------------------------------------------------------------------------
    // Scorecard
    // ---------------------------------------------------------------------------
    function getBaselineText(baseline, keyValue, baselineMode) {
        const baselineEvaluated = baseline === null || baseline === void 0 ? void 0 : baseline.evaluated;
        if (!baseline || baselineEvaluated === undefined) {
            return "";
        }
        else if (baselineMode === "text" ||
            (keyValue === null || keyValue === void 0 ? void 0 : keyValue.type) !== CellValueType.number ||
            baselineEvaluated.type !== CellValueType.number) {
            return baseline.formattedValue;
        }
        else {
            let diff = keyValue.value - baselineEvaluated.value;
            if (baselineMode === "percentage" && diff !== 0) {
                diff = (diff / baselineEvaluated.value) * 100;
            }
            if (baselineMode !== "percentage" && baselineEvaluated.format) {
                return formatValue(diff, baselineEvaluated.format);
            }
            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 === null || baseline === void 0 ? void 0 : baseline.type) !== CellValueType.number ||
            (keyValue === null || keyValue === void 0 ? void 0 : 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 === null || baseline === void 0 ? void 0 : baseline.type) !== CellValueType.number ||
            (keyValue === null || keyValue === void 0 ? void 0 : 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";
    }

    /**
     * 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);
        }
        return [];
    }
    function filterEmptyDataPoints(labels, datasets) {
        const numberOfDataPoints = Math.max(labels.length, ...datasets.map((dataset) => { var _a; return ((_a = dataset.data) === null || _a === void 0 ? void 0 : _a.length) || 0; }));
        const dataPointsIndexes = range(0, numberOfDataPoints).filter((dataPointIndex) => {
            const label = labels[dataPointIndex];
            const values = datasets.map((dataset) => { var _a; return (_a = dataset.data) === null || _a === void 0 ? void 0 : _a[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]),
            })),
        };
    }
    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, truncateLabels = true) {
        return {
            type: chart.type,
            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: {
                    duration: 0, // general animation time
                },
                hover: {
                    animationDuration: 10, // duration of animations when hovering an item
                },
                responsiveAnimationDuration: 0,
                title: {
                    display: !!chart.title,
                    fontSize: 22,
                    fontStyle: "normal",
                    text: _t(chart.title),
                    fontColor,
                },
                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: undefined,
                },
            },
            data: {
                labels: truncateLabels ? labels.map(truncateLabel) : labels,
                datasets: [],
            },
        };
    }
    function getLabelFormat(getters, range) {
        var _a;
        if (!range)
            return undefined;
        return (_a = getters.getCell(range.sheetId, range.zone.left, range.zone.top)) === null || _a === void 0 ? void 0 : _a.evaluated.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) => (val !== undefined && val !== null ? String(val) : "")),
                };
            }
        }
        else if (dataSets.length === 1) {
            const dataLength = getData(getters, dataSets[0]).length;
            for (let i = 0; i < dataLength; 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;
    }
    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.getCell(labelRange.sheetId, labelRange.zone.left, 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;
        }
    }

    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: _lt("Bar"),
    });
    class BarChart extends AbstractChart {
        constructor(definition, sheetId, getters) {
            super(definition, sheetId, getters);
            this.type = "bar";
            this.dataSets = createDataSets(getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);
            this.labelRange = createValidRange(getters, sheetId, definition.labelRange);
            this.background = definition.background;
            this.verticalAxisPosition = definition.verticalAxisPosition;
            this.legendPosition = definition.legendPosition;
            this.stacked = definition.stacked;
        }
        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,
                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,
            };
        }
        getDefinitionForExcel() {
            const dataSets = this.dataSets
                .map((ds) => toExcelDataset(this.getters, ds))
                .filter((ds) => ds.range !== ""); // && range !== INCORRECT_RANGE_STRING ? show incorrect #ref ?
            return {
                ...this.getDefinition(),
                labelRange: this.labelRange
                    ? this.getters.getRangeString(this.labelRange, this.sheetId, true)
                    : undefined,
                backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),
                fontColor: toXlsxHexColor(chartFontColor(this.background)),
                dataSets,
            };
        }
        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) {
        var _a;
        const fontColor = chartFontColor(chart.background);
        const config = getDefaultChartJsRuntime(chart, labels, fontColor);
        const legend = {
            labels: { fontColor },
        };
        if ((!chart.labelRange && chart.dataSets.length === 1) || chart.legendPosition === "none") {
            legend.display = false;
        }
        else {
            legend.position = chart.legendPosition;
        }
        config.options.legend = { ...(_a = config.options) === null || _a === void 0 ? void 0 : _a.legend, ...legend };
        config.options.layout = {
            padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },
        };
        config.options.scales = {
            xAxes: [
                {
                    ticks: {
                        // x axis configuration
                        maxRotation: 60,
                        minRotation: 15,
                        padding: 5,
                        labelOffset: 2,
                        fontColor,
                    },
                },
            ],
            yAxes: [
                {
                    position: chart.verticalAxisPosition,
                    ticks: {
                        fontColor,
                        // y axis configuration
                        beginAtZero: true, // the origin of the y axis is always zero
                    },
                },
            ],
        };
        if (chart.stacked) {
            config.options.scales.xAxes[0].stacked = true;
            config.options.scales.yAxes[0].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);
        ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues));
        const config = getBarConfiguration(chart, labels);
        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 };
    }

    /**
     * Create a function used to create a Chart based on the definition
     */
    function chartFactory(getters) {
        const builders = chartRegistry.getAll();
        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();
        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;
    }

    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: _lt("Gauge"),
    });
    function isDataRangeValid(definition) {
        return definition.dataRange && !rangeReference.test(definition.dataRange)
            ? 37 /* CommandResult.InvalidGaugeDataRange */
            : 0 /* CommandResult.Success */;
    }
    function checkRangeLimits(check, batchValidations) {
        return batchValidations((definition) => {
            if (definition.sectionRule) {
                return check(definition.sectionRule.rangeMin, "rangeMin");
            }
            return 0 /* CommandResult.Success */;
        }, (definition) => {
            if (definition.sectionRule) {
                return check(definition.sectionRule.rangeMax, "rangeMax");
            }
            return 0 /* CommandResult.Success */;
        });
    }
    function checkInflectionPointsValue(check, batchValidations) {
        return batchValidations((definition) => {
            if (definition.sectionRule) {
                return check(definition.sectionRule.lowerInflectionPoint.value, "lowerInflectionPointValue");
            }
            return 0 /* CommandResult.Success */;
        }, (definition) => {
            if (definition.sectionRule) {
                return check(definition.sectionRule.upperInflectionPoint.value, "upperInflectionPointValue");
            }
            return 0 /* CommandResult.Success */;
        });
    }
    function checkRangeMinBiggerThanRangeMax(definition) {
        if (definition.sectionRule) {
            if (Number(definition.sectionRule.rangeMin) >= Number(definition.sectionRule.rangeMax)) {
                return 42 /* CommandResult.GaugeRangeMinBiggerThanRangeMax */;
            }
        }
        return 0 /* CommandResult.Success */;
    }
    function checkEmpty(value, valueName) {
        if (value === "") {
            switch (valueName) {
                case "rangeMin":
                    return 38 /* CommandResult.EmptyGaugeRangeMin */;
                case "rangeMax":
                    return 40 /* CommandResult.EmptyGaugeRangeMax */;
            }
        }
        return 0 /* CommandResult.Success */;
    }
    function checkNaN(value, valueName) {
        if (isNaN(value)) {
            switch (valueName) {
                case "rangeMin":
                    return 39 /* CommandResult.GaugeRangeMinNaN */;
                case "rangeMax":
                    return 41 /* CommandResult.GaugeRangeMaxNaN */;
                case "lowerInflectionPointValue":
                    return 43 /* CommandResult.GaugeLowerInflectionPointNaN */;
                case "upperInflectionPointValue":
                    return 44 /* CommandResult.GaugeUpperInflectionPointNaN */;
            }
        }
        return 0 /* CommandResult.Success */;
    }
    class GaugeChart extends AbstractChart {
        constructor(definition, sheetId, getters) {
            super(definition, sheetId, getters);
            this.type = "gauge";
            this.dataRange = createValidRange(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) {
        const fontColor = chartFontColor(chart.background);
        const config = getDefaultChartJsRuntime(chart, [], fontColor);
        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 = {
            radiusPercentage: 2,
            widthPercentage: 3.2,
            lengthPercentage: 80,
            color: "#000000",
        };
        config.options.valueLabel = {
            display: false,
            formatter: null,
            color: "#FFFFFF",
            backgroundColor: "#000000",
            fontSize: 30,
            borderRadius: 5,
            padding: {
                top: 5,
                right: 5,
                bottom: 5,
                left: 5,
            },
            bottomMarginPercentage: 5,
        };
        return config;
    }
    function createGaugeChartRuntime(chart, getters) {
        const config = getGaugeConfiguration(chart);
        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);
        });
        // There's a bug in gauge lib when the last element in `data` is 0 (i.e. when the range maximum is 0).
        // The value wrongly fallbacks to 1 because 0 is falsy
        // See https://github.com/haiiaaa/chartjs-gauge/pull/33
        // https://github.com/haiiaaa/chartjs-gauge/blob/2ea50541d754d710cb30c2502fa690ac5dc27afd/src/controllers/controller.gauge.js#L52
        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 = null;
        let displayValue = false;
        if (dataRange !== undefined) {
            const cell = getters.getCell(dataRange.sheetId, dataRange.zone.left, dataRange.zone.top);
            if ((cell === null || cell === void 0 ? void 0 : cell.evaluated.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 === null || cell === void 0 ? void 0 : cell.evaluated.value, minNeedleValue - deltaBeyondRangeLimit, maxNeedleValue + deltaBeyondRangeLimit);
                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 moment time format
     */
    const timeFormatMomentCompatible = /^((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) {
        const momentFormat = convertDateFormatForMoment(labelFormat);
        const timeUnit = getBestTimeUnitForScale(labels, momentFormat);
        const displayFormats = {};
        if (timeUnit) {
            displayFormats[timeUnit] = momentFormat;
        }
        return {
            parser: momentFormat,
            displayFormats,
            unit: timeUnit,
        };
    }
    /**
     * Convert the given date format into a format that moment.js understands.
     *
     * https://momentjs.com/docs/#/parsing/string-format/
     */
    function convertDateFormatForMoment(format) {
        format = format.replace(/y/g, "Y");
        format = format.replace(/d/g, "D");
        // "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) {
        const labelDates = labels.map((label) => { var _a; return (_a = parseDateTime(label)) === null || _a === void 0 ? void 0 : _a.jsDate; });
        if (labelDates.some((date) => date === undefined) || labels.length < 2) {
            return undefined;
        }
        const labelsTimestamps = labelDates.map((date) => date.getTime());
        const period = largeMax(labelsTimestamps) - largeMin(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";
    }

    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: _lt("Line"),
    });
    class LineChart extends AbstractChart {
        constructor(definition, sheetId, getters) {
            super(definition, sheetId, getters);
            this.type = "line";
            this.dataSets = createDataSets(this.getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);
            this.labelRange = createValidRange(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.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,
                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,
                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() {
            const dataSets = this.dataSets
                .map((ds) => toExcelDataset(this.getters, ds))
                .filter((ds) => ds.range !== ""); // && range !== INCORRECT_RANGE_STRING ? show incorrect #ref ?
            return {
                ...this.getDefinition(),
                labelRange: this.labelRange
                    ? this.getters.getRangeString(this.labelRange, this.sheetId, true)
                    : undefined,
                backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),
                fontColor: toXlsxHexColor(chartFontColor(this.background)),
                dataSets,
            };
        }
        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(chart, getters) {
        return canBeDateChart(chart, getters) || canBeLinearChart(chart, getters);
    }
    function getChartAxisType(chart, getters) {
        if (isDateChart(chart, getters)) {
            return "time";
        }
        if (isLinearChart(chart, getters)) {
            return "linear";
        }
        return "category";
    }
    function isDateChart(chart, getters) {
        return !chart.labelsAsText && canBeDateChart(chart, getters);
    }
    function isLinearChart(chart, getters) {
        return !chart.labelsAsText && canBeLinearChart(chart, getters);
    }
    function canBeDateChart(chart, getters) {
        var _a;
        if (!chart.labelRange || !chart.dataSets || !canBeLinearChart(chart, getters)) {
            return false;
        }
        const labelFormat = (_a = getters.getCell(chart.labelRange.sheetId, chart.labelRange.zone.left, chart.labelRange.zone.top)) === null || _a === void 0 ? void 0 : _a.evaluated.format;
        return Boolean(labelFormat && timeFormatMomentCompatible.test(labelFormat));
    }
    function canBeLinearChart(chart, getters) {
        if (!chart.labelRange || !chart.dataSets) {
            return false;
        }
        const labels = getters.getRangeValues(chart.labelRange);
        if (labels.some((label) => isNaN(Number(label)) && label)) {
            return false;
        }
        if (labels.every((label) => !label)) {
            return false;
        }
        return true;
    }
    function getLineConfiguration(chart, labels, truncateLabels) {
        var _a;
        const fontColor = chartFontColor(chart.background);
        const config = getDefaultChartJsRuntime(chart, labels, fontColor, truncateLabels);
        const legend = {
            labels: {
                fontColor,
                generateLabels(chart) {
                    const { data } = chart;
                    const labels = window.Chart.defaults.global.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;
        }
        config.options.legend = { ...(_a = config.options) === null || _a === void 0 ? void 0 : _a.legend, ...legend };
        config.options.layout = {
            padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },
        };
        config.options.scales = {
            xAxes: [
                {
                    ticks: {
                        // x axis configuration
                        maxRotation: 60,
                        minRotation: 15,
                        padding: 5,
                        labelOffset: 2,
                        fontColor,
                    },
                },
            ],
            yAxes: [
                {
                    position: chart.verticalAxisPosition,
                    ticks: {
                        fontColor,
                        // y axis configuration
                        beginAtZero: true, // the origin of the y axis is always zero
                    },
                },
            ],
        };
        if (chart.stacked) {
            config.options.scales.yAxes[0].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);
        ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues));
        if (axisType === "time") {
            ({ labels, dataSetsValues } = fixEmptyLabelsForDateCharts(labels, dataSetsValues));
        }
        const truncateLabels = axisType === "category";
        const config = getLineConfiguration(chart, labels, truncateLabels);
        const labelFormat = getLabelFormat(getters, chart.labelRange);
        if (axisType === "time") {
            config.options.scales.xAxes[0].type = "time";
            config.options.scales.xAxes[0].time = getChartTimeOptions(labels, labelFormat);
            config.options.scales.xAxes[0].ticks.maxTicksLimit = 15;
        }
        else if (axisType === "linear") {
            config.options.scales.xAxes[0].type = "linear";
            config.options.scales.xAxes[0].ticks.callback = (value) => formatValue(value, labelFormat);
        }
        const colors = new ChartColors();
        for (let [index, { label, data }] of dataSetsValues.entries()) {
            if (chart.cumulative) {
                let accumulator = 0;
                data = data.map((value) => {
                    if (!isNaN(value)) {
                        accumulator += parseFloat(value);
                        return accumulator;
                    }
                    return value;
                });
            }
            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;
            }
            const backgroundColor = rgbaToHex(backgroundRGBA);
            const dataset = {
                label,
                data,
                lineTension: 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 };
    }

    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: _lt("Pie"),
    });
    class PieChart extends AbstractChart {
        constructor(definition, sheetId, getters) {
            super(definition, sheetId, getters);
            this.type = "pie";
            this.dataSets = createDataSets(getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);
            this.labelRange = createValidRange(getters, sheetId, definition.labelRange);
            this.background = definition.background;
            this.legendPosition = definition.legendPosition;
        }
        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,
            };
        }
        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,
            };
        }
        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() {
            const dataSets = this.dataSets
                .map((ds) => toExcelDataset(this.getters, ds))
                .filter((ds) => ds.range !== ""); // && range !== INCORRECT_RANGE_STRING ? show incorrect #ref ?
            return {
                ...this.getDefinition(),
                labelRange: this.labelRange
                    ? this.getters.getRangeString(this.labelRange, this.sheetId, true)
                    : undefined,
                backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),
                fontColor: toXlsxHexColor(chartFontColor(this.background)),
                verticalAxisPosition: "left",
                dataSets,
            };
        }
        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) {
        var _a;
        const fontColor = chartFontColor(chart.background);
        const config = getDefaultChartJsRuntime(chart, labels, fontColor);
        const legend = {
            labels: { fontColor },
        };
        if ((!chart.labelRange && chart.dataSets.length === 1) || chart.legendPosition === "none") {
            legend.display = false;
        }
        else {
            legend.position = chart.legendPosition;
        }
        config.options.legend = { ...(_a = config.options) === null || _a === void 0 ? void 0 : _a.legend, ...legend };
        config.options.layout = {
            padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },
        };
        config.options.tooltips = {
            callbacks: {
                title: function (tooltipItems, data) {
                    return data.datasets[tooltipItems[0].datasetIndex].label;
                },
            },
        };
        return config;
    }
    function getPieColors(colors, dataSetsValues) {
        const pieColors = [];
        const maxLength = largeMax(dataSetsValues.map((ds) => ds.data.length));
        for (let i = 0; i <= maxLength; i++) {
            pieColors.push(colors.next());
        }
        return pieColors;
    }
    function createPieChartRuntime(chart, getters) {
        const labelValues = getChartLabelValues(getters, chart.dataSets, chart.labelRange);
        let labels = labelValues.formattedValues;
        let dataSetsValues = getChartDatasetValues(getters, chart.dataSets);
        ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues));
        const config = getPieConfiguration(chart, labels);
        const colors = new ChartColors();
        for (let { label, data } of dataSetsValues) {
            const backgroundColor = getPieColors(colors, dataSetsValues);
            const dataset = {
                label,
                data,
                borderColor: chart.background || "#FFFFFF",
                backgroundColor,
            };
            config.data.datasets.push(dataset);
        }
        return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
    }

    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: _lt("Scorecard"),
    });
    function checkKeyValue(definition) {
        return definition.keyValue && !rangeReference.test(definition.keyValue)
            ? 35 /* CommandResult.InvalidScorecardKeyValue */
            : 0 /* CommandResult.Success */;
    }
    function checkBaseline(definition) {
        return definition.baseline && !rangeReference.test(definition.baseline)
            ? 36 /* CommandResult.InvalidScorecardBaseline */
            : 0 /* CommandResult.Success */;
    }
    class ScorecardChart$1 extends AbstractChart {
        constructor(definition, sheetId, getters) {
            super(definition, sheetId, getters);
            this.type = "scorecard";
            this.keyValue = createValidRange(getters, sheetId, definition.keyValue);
            this.baseline = createValidRange(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: "difference",
                baselineColorUp: "#00A04A",
                baselineColorDown: "#DC6965",
                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$1(definition, sheetId, this.getters);
        }
        copyInSheetId(sheetId) {
            const definition = this.getDefinitionWithSpecificRanges(this.baseline, this.keyValue, sheetId);
            return new ScorecardChart$1(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$1(definition, this.sheetId, this.getters);
        }
    }
    function createScorecardChartRuntime(chart, getters) {
        let keyValue = "";
        let formattedKeyValue = "";
        let keyValueCell;
        if (chart.keyValue) {
            const keyValueZone = chart.keyValue.zone;
            keyValueCell = getters.getCell(chart.keyValue.sheetId, keyValueZone.left, keyValueZone.top);
            keyValue = (keyValueCell === null || keyValueCell === void 0 ? void 0 : keyValueCell.evaluated.value) ? String(keyValueCell === null || keyValueCell === void 0 ? void 0 : keyValueCell.evaluated.value) : "";
            formattedKeyValue = (keyValueCell === null || keyValueCell === void 0 ? void 0 : keyValueCell.formattedValue) || "";
        }
        let baselineCell;
        if (chart.baseline) {
            const baselineZone = chart.baseline.zone;
            baselineCell = getters.getCell(chart.baseline.sheetId, baselineZone.left, baselineZone.top);
        }
        const { background, fontColor } = getters.getStyleOfSingleCellChart(chart.background, chart.keyValue);
        return {
            title: _t(chart.title),
            keyValue: formattedKeyValue || keyValue,
            baselineDisplay: getBaselineText(baselineCell, keyValueCell === null || keyValueCell === void 0 ? void 0 : keyValueCell.evaluated, chart.baselineMode),
            baselineArrow: getBaselineArrowDirection(baselineCell === null || baselineCell === void 0 ? void 0 : baselineCell.evaluated, keyValueCell === null || keyValueCell === void 0 ? void 0 : keyValueCell.evaluated, chart.baselineMode),
            baselineColor: getBaselineColor(baselineCell === null || baselineCell === void 0 ? void 0 : baselineCell.evaluated, chart.baselineMode, keyValueCell === null || keyValueCell === void 0 ? void 0 : keyValueCell.evaluated, chart.baselineColorUp, chart.baselineColorDown),
            baselineDescr: chart.baselineDescr ? _t(chart.baselineDescr) : "",
            fontColor,
            background,
            baselineStyle: chart.baselineMode !== "percentage" ? baselineCell === null || baselineCell === void 0 ? void 0 : baselineCell.style : undefined,
            keyValueStyle: keyValueCell === null || keyValueCell === void 0 ? void 0 : keyValueCell.style,
        };
    }

    const PICKER_PADDING = 6;
    const LINE_VERTICAL_PADDING = 1;
    const LINE_HORIZONTAL_PADDING = 6;
    const ITEM_HORIZONTAL_MARGIN = 1;
    const ITEM_EDGE_LENGTH = 18;
    const ITEM_BORDER_WIDTH = 1;
    const ITEMS_PER_LINE = 10;
    const PICKER_WIDTH = ITEMS_PER_LINE * (ITEM_EDGE_LENGTH + ITEM_HORIZONTAL_MARGIN * 2 + 2 * ITEM_BORDER_WIDTH) +
        2 * LINE_HORIZONTAL_PADDING;
    const GRADIENT_WIDTH = PICKER_WIDTH - 2 * LINE_HORIZONTAL_PADDING - 2 * ITEM_BORDER_WIDTH;
    const GRADIENT_HEIGHT = PICKER_WIDTH - 50;
    css /* scss */ `
  .o-color-picker {
    position: absolute;
    top: calc(100% + 5px);
    z-index: ${ComponentsImportance.ColorPicker};
    padding: ${PICKER_PADDING}px 0px;
    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: ${GRADIENT_WIDTH + 2 * PICKER_PADDING}px;

    .o-color-picker-section-name {
      margin: 0px ${ITEM_HORIZONTAL_MARGIN}px;
      padding: 4px ${LINE_HORIZONTAL_PADDING}px;
    }
    .colors-grid {
      display: grid;
      padding: ${LINE_VERTICAL_PADDING}px ${LINE_HORIZONTAL_PADDING}px;
      grid-template-columns: repeat(${ITEMS_PER_LINE}, 1fr);
      grid-gap: ${ITEM_HORIZONTAL_MARGIN * 2}px;
    }
    .o-color-picker-toggler {
      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: 6px;
      display: flex;
      .o-cancel {
        margin: 0px ${ITEM_HORIZONTAL_MARGIN}px;
        border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
        width: 100%;
        padding: 5px;
        font-size: 14px;
        background: white;
        border-radius: 4px;
        &: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 #e0e2e4;
      margin-top: ${MENU_SEPARATOR_PADDING}px;
      margin-bottom: ${MENU_SEPARATOR_PADDING}px;
    }
    input {
      box-sizing: border-box;
      width: 100%;
      border-radius: 4px;
      padding: 4px 23px 4px 10px;
      height: 24px;
      border: 1px solid #c0c0c0;
      margin: 0 2px 0 0;
    }
    input.o-wrong-color {
      border-color: red;
    }
    .o-custom-selector {
      padding: ${LINE_HORIZONTAL_PADDING}px;
      position: relative;
      .o-gradient {
        background: linear-gradient(to bottom, hsl(0 100% 0%), transparent, hsl(0 0% 100%)),
          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%
          );
        border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
        width: ${GRADIENT_WIDTH}px;
        height: ${GRADIENT_HEIGHT}px;
        &:hover {
          cursor: crosshair;
        }
      }
      .o-custom-input-preview {
        padding: 2px ${LINE_VERTICAL_PADDING}px;
        display: flex;
      }
      .o-custom-input-buttons {
        padding: 2px ${LINE_VERTICAL_PADDING}px;
        text-align: right;
      }
      .o-color-preview {
        border: 1px solid #c0c0c0;
        border-radius: 4px;
        width: 100%;
      }
    }
    &.right {
      left: 0;
    }
    &.left {
      right: 0;
    }
    &.center {
      left: calc(50% - ${PICKER_WIDTH / 2}px);
    }
  }
  .o-magnifier-glass {
    position: absolute;
    border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
    border-radius: 50%;
    width: 30px;
    height: 30px;
  }
`;
    function computeCustomColor(ev) {
        return rgbaToHex(hslaToRGBA({
            h: (360 * ev.offsetX) / GRADIENT_WIDTH,
            s: 100,
            l: (100 * ev.offsetY) / GRADIENT_HEIGHT,
            a: 1,
        }));
    }
    class ColorPicker extends owl.Component {
        constructor() {
            super(...arguments);
            this.COLORS = COLOR_PICKER_DEFAULTS;
            this.state = owl.useState({
                showGradient: false,
                currentColor: isColorValid(this.props.currentColor) ? this.props.currentColor : "",
                isCurrentColorInvalid: false,
                style: {
                    display: "none",
                    background: "#ffffff",
                    left: "0",
                    top: "0",
                },
            });
        }
        get colorPickerStyle() {
            if (this.props.maxHeight === undefined)
                return "";
            if (this.props.maxHeight <= 0) {
                return cssPropertiesToCss({ display: "none" });
            }
            return cssPropertiesToCss({
                "max-height": `${this.props.maxHeight}px`,
            });
        }
        onColorClick(color) {
            if (color) {
                this.props.onColorPicked(toHex(color));
            }
        }
        getCheckMarkColor() {
            return chartFontColor(this.props.currentColor);
        }
        resetColor() {
            this.props.onColorPicked("");
        }
        setCustomColor(ev) {
            if (!isColorValid(this.state.currentColor)) {
                ev.stopPropagation();
                this.state.isCurrentColorInvalid = true;
                return;
            }
            const color = toHex(this.state.currentColor);
            this.state.isCurrentColorInvalid = false;
            this.props.onColorPicked(color);
            this.state.currentColor = color;
        }
        toggleColorPicker() {
            this.state.showGradient = !this.state.showGradient;
        }
        computeCustomColor(ev) {
            this.state.isCurrentColorInvalid = false;
            this.state.currentColor = computeCustomColor(ev);
        }
        hideMagnifier(_ev) {
            this.state.style.display = "none";
        }
        showMagnifier(_ev) {
            this.state.style.display = "block";
        }
        moveMagnifier(ev) {
            this.state.style.background = computeCustomColor(ev);
            const shiftFromCursor = 10;
            this.state.style.left = `${ev.offsetX + shiftFromCursor}px`;
            this.state.style.top = `${ev.offsetY + shiftFromCursor}px`;
        }
        get magnifyingGlassStyle() {
            const { display, background, left, top } = this.state.style;
            return `display:${display};${display === "block" ? `background-color:${background};left:${left};top:${top};` : ""}`;
        }
        isSameColor(color1, color2) {
            return isSameColor(color1, color2);
        }
    }
    ColorPicker.template = "o-spreadsheet-ColorPicker";

    class LineBarPieDesignPanel extends owl.Component {
        constructor() {
            super(...arguments);
            this.state = owl.useState({
                fillColorTool: false,
            });
        }
        onClick(ev) {
            this.state.fillColorTool = false;
        }
        setup() {
            owl.useExternalListener(window, "click", this.onClick);
        }
        toggleColorPicker() {
            this.state.fillColorTool = !this.state.fillColorTool;
        }
        updateBackgroundColor(color) {
            this.props.updateChart(this.props.figureId, {
                background: color,
            });
        }
        updateTitle(ev) {
            this.props.updateChart(this.props.figureId, {
                title: ev.target.value,
            });
        }
        updateSelect(attr, ev) {
            this.props.updateChart(this.props.figureId, {
                [attr]: ev.target.value,
            });
        }
    }
    LineBarPieDesignPanel.template = "o-spreadsheet-LineBarPieDesignPanel";
    LineBarPieDesignPanel.components = { ColorPicker };

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

    class GaugeChartConfigPanel extends owl.Component {
        constructor() {
            super(...arguments);
            this.state = owl.useState({
                dataRangeDispatchResult: undefined,
            });
            this.dataRange = this.props.definition.dataRange;
        }
        get configurationErrorMessages() {
            var _a;
            const cancelledReasons = [...(((_a = this.state.dataRangeDispatchResult) === null || _a === void 0 ? void 0 : _a.reasons) || [])];
            return cancelledReasons.map((error) => ChartTerms.Errors[error] || ChartTerms.Errors.Unexpected);
        }
        get isDataRangeInvalid() {
            var _a;
            return !!((_a = this.state.dataRangeDispatchResult) === null || _a === void 0 ? void 0 : _a.isCancelledBecause(37 /* CommandResult.InvalidGaugeDataRange */));
        }
        onDataRangeChanged(ranges) {
            this.dataRange = ranges[0];
        }
        updateDataRange() {
            this.state.dataRangeDispatchResult = this.props.updateChart(this.props.figureId, {
                dataRange: this.dataRange,
            });
        }
    }
    GaugeChartConfigPanel.template = "o-spreadsheet-GaugeChartConfigPanel";
    GaugeChartConfigPanel.components = { SelectionInput };

    css /* scss */ `
  .o-gauge-color-set {
    .o-gauge-color-set-color-button {
      display: inline-block;
      border: 1px solid #dadce0;
      border-radius: 4px;
      cursor: pointer;
      padding: 1px 2px;
    }
    .o-gauge-color-set-color-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-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 {
        constructor() {
            super(...arguments);
            this.state = owl.useState({
                openedMenu: undefined,
                sectionRuleDispatchResult: undefined,
            });
        }
        get designErrorMessages() {
            var _a;
            const cancelledReasons = [...(((_a = this.state.sectionRuleDispatchResult) === null || _a === void 0 ? void 0 : _a.reasons) || [])];
            return cancelledReasons.map((error) => ChartTerms.Errors[error] || ChartTerms.Errors.Unexpected);
        }
        onClick(ev) {
            this.state.openedMenu = undefined;
        }
        setup() {
            owl.useExternalListener(window, "click", this.onClick);
        }
        updateBackgroundColor(color) {
            this.state.openedMenu = undefined;
            this.props.updateChart(this.props.figureId, {
                background: color,
            });
        }
        updateTitle(ev) {
            this.props.updateChart(this.props.figureId, {
                title: ev.target.value,
            });
        }
        isRangeMinInvalid() {
            var _a, _b, _c;
            return !!(((_a = this.state.sectionRuleDispatchResult) === null || _a === void 0 ? void 0 : _a.isCancelledBecause(38 /* CommandResult.EmptyGaugeRangeMin */)) ||
                ((_b = this.state.sectionRuleDispatchResult) === null || _b === void 0 ? void 0 : _b.isCancelledBecause(39 /* CommandResult.GaugeRangeMinNaN */)) ||
                ((_c = this.state.sectionRuleDispatchResult) === null || _c === void 0 ? void 0 : _c.isCancelledBecause(42 /* CommandResult.GaugeRangeMinBiggerThanRangeMax */)));
        }
        isRangeMaxInvalid() {
            var _a, _b, _c;
            return !!(((_a = this.state.sectionRuleDispatchResult) === null || _a === void 0 ? void 0 : _a.isCancelledBecause(40 /* CommandResult.EmptyGaugeRangeMax */)) ||
                ((_b = this.state.sectionRuleDispatchResult) === null || _b === void 0 ? void 0 : _b.isCancelledBecause(41 /* CommandResult.GaugeRangeMaxNaN */)) ||
                ((_c = this.state.sectionRuleDispatchResult) === null || _c === void 0 ? void 0 : _c.isCancelledBecause(42 /* CommandResult.GaugeRangeMinBiggerThanRangeMax */)));
        }
        // ---------------------------------------------------------------------------
        // COLOR_SECTION_TEMPLATE
        // ---------------------------------------------------------------------------
        get isLowerInflectionPointInvalid() {
            var _a, _b;
            return !!(((_a = this.state.sectionRuleDispatchResult) === null || _a === void 0 ? void 0 : _a.isCancelledBecause(43 /* CommandResult.GaugeLowerInflectionPointNaN */)) ||
                ((_b = this.state.sectionRuleDispatchResult) === null || _b === void 0 ? void 0 : _b.isCancelledBecause(45 /* CommandResult.GaugeLowerBiggerThanUpper */)));
        }
        get isUpperInflectionPointInvalid() {
            var _a, _b;
            return !!(((_a = this.state.sectionRuleDispatchResult) === null || _a === void 0 ? void 0 : _a.isCancelledBecause(44 /* CommandResult.GaugeUpperInflectionPointNaN */)) ||
                ((_b = this.state.sectionRuleDispatchResult) === null || _b === void 0 ? void 0 : _b.isCancelledBecause(45 /* CommandResult.GaugeLowerBiggerThanUpper */)));
        }
        updateInflectionPointValue(attr, ev) {
            const sectionRule = deepCopy(this.props.definition.sectionRule);
            sectionRule[attr].value = ev.target.value;
            this.updateSectionRule(sectionRule);
        }
        updateInflectionPointType(attr, ev) {
            const sectionRule = deepCopy(this.props.definition.sectionRule);
            sectionRule[attr].type = ev.target.value;
            this.updateSectionRule(sectionRule);
        }
        updateSectionColor(target, color) {
            const sectionRule = deepCopy(this.props.definition.sectionRule);
            sectionRule.colors[target] = color;
            this.updateSectionRule(sectionRule);
            this.closeMenus();
        }
        updateRangeMin(ev) {
            let sectionRule = deepCopy(this.props.definition.sectionRule);
            sectionRule = {
                ...sectionRule,
                rangeMin: ev.target.value,
            };
            this.updateSectionRule(sectionRule);
        }
        updateRangeMax(ev) {
            let sectionRule = deepCopy(this.props.definition.sectionRule);
            sectionRule = {
                ...sectionRule,
                rangeMax: ev.target.value,
            };
            this.updateSectionRule(sectionRule);
        }
        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,
            });
        }
        closeMenus() {
            this.state.openedMenu = undefined;
        }
    }
    GaugeChartDesignPanel.template = "o-spreadsheet-GaugeChartDesignPanel";
    GaugeChartDesignPanel.components = { ColorPicker };

    class LineConfigPanel extends LineBarPieConfigPanel {
        get canTreatLabelsAsText() {
            const chart = this.env.model.getters.getChart(this.props.figureId);
            if (chart && chart instanceof LineChart) {
                return canChartParseLabels(chart, 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,
            });
        }
        onUpdateCumulative(ev) {
            this.props.updateChart(this.props.figureId, {
                cumulative: ev.target.checked,
            });
        }
    }
    LineConfigPanel.template = "o-spreadsheet-LineConfigPanel";

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

    class ScorecardChartConfigPanel extends owl.Component {
        constructor() {
            super(...arguments);
            this.state = owl.useState({
                keyValueDispatchResult: undefined,
                baselineDispatchResult: undefined,
            });
            this.keyValue = this.props.definition.keyValue;
            this.baseline = this.props.definition.baseline;
        }
        get errorMessages() {
            var _a, _b;
            const cancelledReasons = [
                ...(((_a = this.state.keyValueDispatchResult) === null || _a === void 0 ? void 0 : _a.reasons) || []),
                ...(((_b = this.state.baselineDispatchResult) === null || _b === void 0 ? void 0 : _b.reasons) || []),
            ];
            return cancelledReasons.map((error) => ChartTerms.Errors[error] || ChartTerms.Errors.Unexpected);
        }
        get isKeyValueInvalid() {
            var _a;
            return !!((_a = this.state.keyValueDispatchResult) === null || _a === void 0 ? void 0 : _a.isCancelledBecause(35 /* CommandResult.InvalidScorecardKeyValue */));
        }
        get isBaselineInvalid() {
            var _a;
            return !!((_a = this.state.keyValueDispatchResult) === null || _a === void 0 ? void 0 : _a.isCancelledBecause(36 /* CommandResult.InvalidScorecardBaseline */));
        }
        onKeyValueRangeChanged(ranges) {
            this.keyValue = ranges[0];
        }
        updateKeyValueRange() {
            this.state.keyValueDispatchResult = this.props.updateChart(this.props.figureId, {
                keyValue: this.keyValue,
            });
        }
        onBaselineRangeChanged(ranges) {
            this.baseline = ranges[0];
        }
        updateBaselineRange() {
            this.state.baselineDispatchResult = this.props.updateChart(this.props.figureId, {
                baseline: this.baseline,
            });
        }
        updateBaselineMode(ev) {
            this.props.updateChart(this.props.figureId, { baselineMode: ev.target.value });
        }
    }
    ScorecardChartConfigPanel.template = "o-spreadsheet-ScorecardChartConfigPanel";
    ScorecardChartConfigPanel.components = { SelectionInput };

    class ScorecardChartDesignPanel extends owl.Component {
        constructor() {
            super(...arguments);
            this.state = owl.useState({
                openedColorPicker: undefined,
            });
        }
        onClick(ev) {
            this.state.openedColorPicker = undefined;
        }
        setup() {
            owl.useExternalListener(window, "click", this.onClick);
        }
        updateTitle(ev) {
            this.props.updateChart(this.props.figureId, {
                title: ev.target.value,
            });
        }
        updateBaselineDescr(ev) {
            this.props.updateChart(this.props.figureId, { baselineDescr: ev.target.value });
        }
        openColorPicker(colorPickerId) {
            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.state.openedColorPicker = undefined;
        }
    }
    ScorecardChartDesignPanel.template = "o-spreadsheet-ScorecardChartDesignPanel";
    ScorecardChartDesignPanel.components = { ColorPicker };

    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;
      }
    }

    .o-with-color-picker {
      position: relative;
    }
    .o-with-color-picker > span {
      border-bottom: 4px solid;
    }
  }
`;
    class ChartPanel extends owl.Component {
        get figureId() {
            return this.state.figureId;
        }
        setup() {
            const selectedFigureId = this.env.model.getters.getSelectedFigureId();
            if (!selectedFigureId) {
                throw new Error(_lt("Cannot open the chart side panel while no chart are selected"));
            }
            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),
            });
        }
        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.template = "o-spreadsheet-ChartPanel";

    // -----------------------------------------------------------------------------
    // 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 {
        constructor() {
            super(...arguments);
            this.icons = ICONS;
            this.iconSets = ICON_SETS;
        }
        onIconClick(icon) {
            if (icon) {
                this.props.onIconPicked(icon);
            }
        }
    }
    IconPicker.template = "o-spreadsheet-IconPicker";

    // TODO vsc: add ordering of rules
    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-cursor-ptr {
      cursor: pointer;
    }
    .o-cf-preview {
      background-color: #fff;
      border-bottom: 1px solid #ccc;
      display: flex;
      height: 60px;
      padding: 10px;
      position: relative;
      &:hover {
        background-color: rgba(0, 0, 0, 0.08);
      }
      &:not(:hover) .o-cf-delete-button {
        display: none;
      }
      .o-cf-preview-image {
        border: 1px solid lightgrey;
        height: 50px;
        line-height: 50px;
        margin-right: 15px;
        margin-top: 3px;
        position: absolute;
        text-align: center;
        width: 50px;
      }
      .o-cf-preview-icon {
        border: 1px solid lightgrey;
        position: absolute;
        height: 50px;
        line-height: 50px;
        margin-right: 15px;
        margin-top: 3px;
        display: flex;
        justify-content: space-around;
        align-items: center;
      }
      .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;
          overflow: hidden;
          text-overflow: ellipsis;
          font-weight: 600;
          color: #303030;
          max-height: 2.8em;
          line-height: 1.4em;
        }
        .o-cf-preview-range {
          text-overflow: ellipsis;
          font-size: 12px;
          overflow: hidden;
        }
      }
      .o-cf-delete {
        color: dimgrey;
        left: 90%;
        top: 39%;
        position: absolute;
      }
      .o-cf-reorder {
        color: gray;
        left: 90%;
        position: absolute;
        height: 100%;
        width: 10%;
      }
      .o-cf-reorder-button:hover {
        cursor: pointer;
        background-color: rgba(0, 0, 0, 0.08);
      }
      .o-cf-reorder-button-up {
        width: 15px;
        height: 20px;
        padding: 5px;
        padding-top: 0px;
      }
      .o-cf-reorder-button-down {
        width: 15px;
        height: 20px;
        bottom: 20px;
        padding: 5px;
        padding-top: 0px;
        position: absolute;
      }
    }
    .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-btn-link {
      font-size: 14px;
      padding: 20px 24px 11px 24px;
      height: 44px;
      cursor: pointer;
      text-decoration: none;
    }
    .o-cf-btn-link:hover {
      color: #003a39;
      text-decoration: none;
    }
    .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 {
      pointer-events: all;
    }
  }
  .o-cf-color-scale-editor {
    .o-threshold {
      display: flex;
      flex-direction: horizontal;
      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 ConditionalFormattingPanel extends owl.Component {
        constructor() {
            super(...arguments);
            this.icons = ICONS;
            this.cellIsOperators = CellIsOperators;
            this.iconSets = ICON_SETS;
            this.getTextDecoration = getTextDecoration;
            this.colorNumberString = colorNumberString;
        }
        setup() {
            this.activeSheetId = this.env.model.getters.getActiveSheetId();
            this.state = owl.useState({
                mode: "list",
                errors: [],
                rules: this.getDefaultRules(),
            });
            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();
                    }
                }
            });
            owl.useExternalListener(window, "click", this.closeMenus);
        }
        get conditionalFormats() {
            return this.env.model.getters.getConditionalFormats(this.env.model.getters.getActiveSheetId());
        }
        get isRangeValid() {
            return this.state.errors.includes(24 /* CommandResult.EmptyRange */);
        }
        errorMessage(error) {
            return CfTerms.Errors[error] || CfTerms.Errors.Unexpected;
        }
        /**
         * Switch to the list view
         */
        switchToList() {
            this.state.mode = "list";
            this.state.currentCF = undefined;
            this.state.currentCFType = undefined;
            this.state.errors = [];
            this.state.rules = this.getDefaultRules();
        }
        getStyle(rule) {
            if (rule.type === "CellIsRule") {
                const fontWeight = rule.style.bold ? "bold" : "normal";
                const fontDecoration = getTextDecoration(rule.style);
                const fontStyle = rule.style.italic ? "italic" : "normal";
                const color = rule.style.textColor || "none";
                const backgroundColor = rule.style.fillColor || "none";
                return `font-weight:${fontWeight};
               text-decoration:${fontDecoration};
               font-style:${fontStyle};
               color:${color};
               background-color:${backgroundColor};`;
            }
            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;
                default:
                    return "";
            }
        }
        saveConditionalFormat() {
            if (this.state.currentCF) {
                const invalidRanges = this.state.currentCF.ranges.some((xc) => !xc.match(rangeReference));
                if (invalidRanges) {
                    this.state.errors = [25 /* CommandResult.InvalidRange */];
                    return;
                }
                const sheetId = this.env.model.getters.getActiveSheetId();
                const result = this.env.model.dispatch("ADD_CONDITIONAL_FORMAT", {
                    cf: {
                        rule: this.getEditorRule(),
                        id: this.state.mode === "edit"
                            ? this.state.currentCF.id
                            : this.env.model.uuidGenerator.smallUuid(),
                    },
                    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.switchToList();
                }
            }
        }
        /**
         * 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;
            }
            throw new Error(`Invalid cf type: ${this.state.currentCFType}`);
        }
        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",
                    },
                },
            };
        }
        /**
         * Create a new CF, a CellIsRule by default
         */
        addConditionalFormat() {
            this.state.mode = "add";
            this.state.currentCFType = "CellIsRule";
            this.state.currentCF = {
                id: this.env.model.uuidGenerator.smallUuid(),
                ranges: this.env.model.getters
                    .getSelectedZones()
                    .map((zone) => this.env.model.getters.zoneToXC(this.env.model.getters.getActiveSheetId(), zone)),
            };
        }
        /**
         * Delete a CF
         */
        deleteConditionalFormat(cf) {
            this.env.model.dispatch("REMOVE_CONDITIONAL_FORMAT", {
                id: cf.id,
                sheetId: this.env.model.getters.getActiveSheetId(),
            });
        }
        /**
         * Edit an existing CF. Return without doing anything in reorder mode.
         */
        editConditionalFormat(cf) {
            if (this.state.mode === "reorder")
                return;
            this.state.mode = "edit";
            this.state.currentCF = cf;
            this.state.currentCFType = cf.rule.type;
            switch (cf.rule.type) {
                case "CellIsRule":
                    this.state.rules.cellIs = cf.rule;
                    break;
                case "ColorScaleRule":
                    this.state.rules.colorScale = cf.rule;
                    break;
                case "IconSetRule":
                    this.state.rules.iconSet = cf.rule;
                    break;
            }
        }
        /**
         * Reorder existing CFs
         */
        reorderConditionalFormats() {
            this.state.mode = "reorder";
        }
        reorderRule(cf, direction) {
            this.env.model.dispatch("MOVE_CONDITIONAL_FORMAT", {
                cfId: cf.id,
                direction: direction,
                sheetId: this.env.model.getters.getActiveSheetId(),
            });
        }
        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() {
            var _a;
            return !!((_a = this.state.errors) === null || _a === void 0 ? void 0 : _a.includes(52 /* CommandResult.FirstArgMissing */));
        }
        get isValue2Invalid() {
            var _a;
            return !!((_a = this.state.errors) === null || _a === void 0 ? void 0 : _a.includes(53 /* CommandResult.SecondArgMissing */));
        }
        toggleStyle(tool) {
            const style = this.state.rules.cellIs.style;
            style[tool] = !style[tool];
            this.closeMenus();
        }
        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(59 /* CommandResult.MinInvalidFormula */) ||
                        this.state.errors.includes(51 /* CommandResult.MinBiggerThanMid */) ||
                        this.state.errors.includes(48 /* CommandResult.MinBiggerThanMax */) ||
                        this.state.errors.includes(54 /* CommandResult.MinNaN */));
                case "midpoint":
                    return (this.state.errors.includes(60 /* CommandResult.MidInvalidFormula */) ||
                        this.state.errors.includes(55 /* CommandResult.MidNaN */) ||
                        this.state.errors.includes(50 /* CommandResult.MidBiggerThanMax */));
                case "maximum":
                    return (this.state.errors.includes(61 /* CommandResult.MaxInvalidFormula */) ||
                        this.state.errors.includes(56 /* CommandResult.MaxNaN */));
                default:
                    return false;
            }
        }
        setColorScaleColor(target, color) {
            if (!isColorValid(color)) {
                return;
            }
            const point = this.state.rules.colorScale[target];
            if (point) {
                point.color = Number.parseInt(color.substr(1), 16);
            }
            this.closeMenus();
        }
        getPreviewGradient() {
            var _a;
            const rule = this.state.rules.colorScale;
            const minColor = colorNumberString(rule.minimum.color);
            const midColor = colorNumberString(((_a = rule.midpoint) === null || _a === void 0 ? void 0 : _a.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(58 /* CommandResult.ValueLowerInflectionNaN */) ||
                        this.state.errors.includes(63 /* CommandResult.ValueLowerInvalidFormula */) ||
                        this.state.errors.includes(49 /* CommandResult.LowerBiggerThanUpper */));
                case "upperInflectionPoint":
                    return (this.state.errors.includes(57 /* CommandResult.ValueUpperInflectionNaN */) ||
                        this.state.errors.includes(62 /* CommandResult.ValueUpperInvalidFormula */) ||
                        this.state.errors.includes(49 /* 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;
        }
    }
    ConditionalFormattingPanel.template = "o-spreadsheet-ConditionalFormattingPanel";
    ConditionalFormattingPanel.components = { SelectionInput, IconPicker, ColorPicker };

    css /* scss */ `
  .o-custom-currency {
    .o-format-proposals {
      color: black;
    }
  }
`;
    class CustomCurrencyPanel extends owl.Component {
        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 proposalBases = this.initProposalBases(currency.decimalPlaces);
            const firstPosition = currency.position;
            const secondPosition = currency.position === "before" ? "after" : "before";
            const symbol = this.state.currencySymbol.trim() ? this.state.currencySymbol : "";
            const code = this.state.currencyCode.trim() ? this.state.currencyCode : "";
            return code || symbol
                ? [
                    ...this.createFormatProposals(proposalBases, symbol, code, firstPosition),
                    ...this.createFormatProposals(proposalBases, symbol, code, secondPosition),
                ]
                : [];
        }
        get isSameFormat() {
            const selectedFormat = this.formatProposals[this.state.selectedFormatIndex];
            return selectedFormat ? selectedFormat.format === this.getCommonFormat() : false;
        }
        async updateAvailableCurrencies() {
            var _a, _b;
            if (currenciesRegistry.getAll().length === 0) {
                const currencies = (await ((_b = (_a = this.env).loadCurrencies) === null || _b === void 0 ? void 0 : _b.call(_a))) || [];
                currencies.forEach((currency, index) => {
                    currenciesRegistry.add(index.toString(), currency);
                });
            }
            const emptyCurrency = {
                name: this.env._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;
        }
        initProposalBases(decimalPlaces) {
            const result = [{ format: "#,##0", example: "1,000" }];
            const decimalRepresentation = decimalPlaces ? "." + "0".repeat(decimalPlaces) : "";
            if (decimalRepresentation) {
                result.push({
                    format: "#,##0" + decimalRepresentation,
                    example: "1,000" + decimalRepresentation,
                });
            }
            return result;
        }
        createFormatProposals(proposalBases, symbol, code, position) {
            let formatProposals = [];
            // 1 - add proposal with symbol and without code
            if (symbol) {
                for (let base of proposalBases) {
                    formatProposals.push(this.createFormatProposal(position, base.example, base.format, symbol));
                }
            }
            // 2 - if code exist --> add more proposal with symbol and with code
            if (code) {
                for (let base of proposalBases) {
                    const expression = (position === "after" ? " " : "") + code + " " + symbol;
                    formatProposals.push(this.createFormatProposal(position, base.example, base.format, expression));
                }
            }
            return formatProposals;
        }
        createFormatProposal(position, baseExample, formatBase, expression) {
            const formatExpression = "[$" + expression + "]";
            return {
                example: position === "before" ? expression + baseExample : baseExample + expression,
                format: position === "before" ? formatExpression + formatBase : formatBase + formatExpression,
            };
        }
        getCommonFormat() {
            var _a;
            const selectedZones = this.env.model.getters.getSelectedZones();
            const sheetId = this.env.model.getters.getActiveSheetId();
            const cells = selectedZones
                .map((zone) => this.env.model.getters.getCellsInZone(sheetId, zone))
                .flat();
            const firstFormat = (_a = cells[0]) === null || _a === void 0 ? void 0 : _a.format;
            return cells.every((cell) => (cell === null || cell === void 0 ? void 0 : cell.format) === firstFormat) ? firstFormat : undefined;
        }
        currencyDisplayName(currency) {
            return currency.name + (currency.code ? ` (${currency.code})` : "");
        }
    }
    CustomCurrencyPanel.template = "o-spreadsheet-CustomCurrencyPanel";

    css /* scss */ `
  .o-find-and-replace {
    .o-far-item {
      display: block;
      .o-far-checkbox {
        display: inline-block;
        .o-far-input {
          vertical-align: middle;
        }
        .o-far-label {
          position: relative;
          top: 1.5px;
          padding-left: 4px;
        }
      }
    }
    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 {
        constructor() {
            super(...arguments);
            this.state = owl.useState(this.initialState());
            this.showFormulaState = false;
            this.findAndReplaceRef = owl.useRef("findAndReplace");
        }
        get hasSearchResult() {
            return this.env.model.getters.getCurrentSelectedMatchIndex() !== null;
        }
        setup() {
            this.showFormulaState = this.env.model.getters.shouldShowFormulas();
            owl.onMounted(() => this.focusInput());
            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();
                this.onSelectNextCell();
            }
        }
        onKeydownReplace(ev) {
            if (ev.key === "Enter") {
                ev.preventDefault();
                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
        // ---------------------------------------------------------------------------
        focusInput() {
            const el = this.findAndReplaceRef.el;
            const input = el.querySelector(`input`);
            if (input) {
                input.focus();
            }
        }
        initialState() {
            return {
                toSearch: "",
                replaceWith: "",
                searchOptions: {
                    matchCase: false,
                    exactMatch: false,
                    searchFormulas: false,
                },
            };
        }
    }
    FindAndReplacePanel.template = "o-spreadsheet-FindAndReplacePanel";

    const sidePanelRegistry = new Registry();
    sidePanelRegistry.add("ConditionalFormatting", {
        title: _lt("Conditional formatting"),
        Body: ConditionalFormattingPanel,
    });
    sidePanelRegistry.add("ChartPanel", {
        title: _lt("Chart"),
        Body: ChartPanel,
    });
    sidePanelRegistry.add("FindAndReplace", {
        title: _lt("Find and Replace"),
        Body: FindAndReplacePanel,
    });
    sidePanelRegistry.add("CustomCurrency", {
        title: _lt("Custom currency format"),
        Body: CustomCurrencyPanel,
    });

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

    /* 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;
    css /* scss */ `
  div.o-scorecard {
    font-family: ${DEFAULT_FONT};
    user-select: none;
    background-color: white;
    display: flex;
    flex-direction: column;
    box-sizing: border-box;

    .o-scorecard-content {
      display: flex;
      flex-direction: column;
      height: 100%;
      justify-content: center;
      text-align: center;
    }

    .o-title-text {
      text-align: left;
      height: ${LINE_HEIGHT + "em"};
      line-height: ${LINE_HEIGHT + "em"};
      overflow: hidden;
      white-space: nowrap;
    }

    .o-key-text {
      line-height: ${LINE_HEIGHT + "em"};
      height: ${LINE_HEIGHT + "em"};
      overflow: hidden;
      white-space: nowrap;
    }

    .o-cf-icon {
      display: inline-block;
      width: 0.65em;
      height: 1em;
      line-height: 1em;
      padding-bottom: 0.07em;
      padding-right: 3px;
    }

    .o-baseline-text {
      line-height: ${LINE_HEIGHT + "em"};
      height: ${LINE_HEIGHT + "em"};
      overflow: hidden;
      white-space: nowrap;

      .o-baseline-text-description {
        white-space: pre;
      }
    }
  }
`;
    class ScorecardChart extends owl.Component {
        constructor() {
            super(...arguments);
            this.ctx = document.createElement("canvas").getContext("2d");
        }
        get runtime() {
            return this.env.model.getters.getChartRuntime(this.props.figure.id);
        }
        get title() {
            return this.runtime.title;
        }
        get keyValue() {
            return this.runtime.keyValue;
        }
        get baseline() {
            return this.runtime.baselineDisplay;
        }
        get baselineDescr() {
            const baselineDescr = this.runtime.baselineDescr || "";
            return this.baseline && baselineDescr ? " " + baselineDescr : baselineDescr;
        }
        get baselineArrowDirection() {
            return this.runtime.baselineArrow;
        }
        get backgroundColor() {
            return this.runtime.background;
        }
        get primaryFontColor() {
            return this.runtime.fontColor;
        }
        get secondaryFontColor() {
            return relativeLuminance(this.backgroundColor) > 0.3 ? "#525252" : "#C8C8C8";
        }
        get figure() {
            return this.props.figure;
        }
        get chartStyle() {
            return `
      padding:${this.chartPadding}px;
      background:${this.backgroundColor};
    `;
        }
        get chartContentStyle() {
            return `
      height:${this.getDrawableHeight()}px;
    `;
        }
        get chartPadding() {
            return this.props.figure.width * CHART_PADDING_RATIO;
        }
        getTextStyles() {
            var _a, _b, _c;
            // If the widest text overflows horizontally, scale it down, and apply the same scaling factors to all the other fonts.
            const maxLineWidth = this.props.figure.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.ctx, this));
            let scalingFactor = fontSizeMatchingWidth / baseFontSize;
            // Fonts sizes in px
            const keyFontSize = new KeyValueElement().getElementMaxFontSize(this.getDrawableHeight(), this) * scalingFactor;
            const baselineFontSize = new BaselineElement().getElementMaxFontSize(this.getDrawableHeight(), this) * scalingFactor;
            return {
                titleStyle: this.getTextStyle({
                    fontSize: TITLE_FONT_SIZE,
                    color: this.secondaryFontColor,
                }),
                keyStyle: this.getTextStyle({
                    fontSize: keyFontSize,
                    cellStyle: (_a = this.runtime) === null || _a === void 0 ? void 0 : _a.keyValueStyle,
                    color: this.primaryFontColor,
                }),
                baselineStyle: this.getTextStyle({
                    fontSize: baselineFontSize,
                }),
                baselineValueStyle: this.getTextStyle({
                    fontSize: baselineFontSize,
                    cellStyle: (_b = this.runtime) === null || _b === void 0 ? void 0 : _b.baselineStyle,
                    color: ((_c = this.runtime) === null || _c === void 0 ? void 0 : _c.baselineColor) || this.secondaryFontColor,
                }),
                baselineDescrStyle: this.getTextStyle({
                    fontSize: baselineFontSize * BASELINE_DESCR_FONT_RATIO,
                    color: this.secondaryFontColor,
                }),
            };
        }
        /** Return an CSS style string corresponding to the given arguments */
        getTextStyle(args) {
            const cssAttributes = cellTextStyleToCss(args.cellStyle);
            cssAttributes["font-size"] = `${args.fontSize}px`;
            cssAttributes["display"] = "inline-block";
            if (!cssAttributes["color"] && args.color) {
                cssAttributes["color"] = args.color;
            }
            return cssPropertiesToCss(cssAttributes);
        }
        /** Get the height of the chart minus all the vertical paddings */
        getDrawableHeight() {
            const verticalPadding = 2 * this.chartPadding;
            let availableHeight = this.props.figure.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();
            const keyValue = new KeyValueElement();
            return baseline.getElementWidth(BASELINE_BOX_HEIGHT_RATIO, this.ctx, this) >
                keyValue.getElementWidth(KEY_BOX_HEIGHT_RATIO, this.ctx, this)
                ? baseline
                : keyValue;
        }
    }
    ScorecardChart.template = "o-spreadsheet-ScorecardChart";
    class BaselineElement {
        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.baselineArrowDirection !== "neutral" ? "A " + baselineStr : baselineStr;
            ctx.font = `${fontSize}px ${DEFAULT_FONT}`;
            let textWidth = ctx.measureText(largeText).width;
            // Baseline descr font size should be smaller than baseline font size
            ctx.font = `${fontSize * BASELINE_DESCR_FONT_RATIO}px ${DEFAULT_FONT}`;
            textWidth += ctx.measureText(chart.baselineDescr).width;
            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 {
        getElementWidth(fontSize, ctx, chart) {
            if (!chart.runtime)
                return 0;
            const str = chart.keyValue || "";
            ctx.font = `${fontSize}px ${DEFAULT_FONT}`;
            return ctx.measureText(str).width;
        }
        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;
        }
    }
    chartComponentRegistry.add("scorecard", ScorecardChart);

    // -----------------------------------------------------------------------------
    // STYLE
    // -----------------------------------------------------------------------------
    css /* scss */ `
  .o-chart-container {
    width: 100%;
    height: 100%;
    position: relative;

    .o-chart-menu {
      right: 0px;
      display: none;
      position: absolute;
      padding: 5px;
    }

    .o-chart-menu-item {
      cursor: pointer;
    }
  }
  .o-figure.active:focus,
  .o-figure:hover {
    .o-chart-container {
      .o-chart-menu {
        display: flex;
      }
    }
  }
`;
    class ChartFigure extends owl.Component {
        constructor() {
            super(...arguments);
            this.menuState = owl.useState({ isOpen: false, position: null, menuItems: [] });
            this.chartContainerRef = owl.useRef("chartContainer");
            this.menuButtonRef = owl.useRef("menuButton");
            this.menuButtonPosition = useAbsolutePosition(this.menuButtonRef);
            this.position = useAbsolutePosition(this.chartContainerRef);
        }
        getMenuItemRegistry() {
            const registry = new MenuItemRegistry();
            registry.add("edit", {
                name: _lt("Edit"),
                sequence: 1,
                action: () => {
                    this.env.model.dispatch("SELECT_FIGURE", { id: this.props.figure.id });
                    this.env.openSidePanel("ChartPanel");
                },
            });
            registry.add("copy", {
                name: _lt("Copy"),
                sequence: 2,
                action: async () => {
                    this.env.model.dispatch("SELECT_FIGURE", { id: this.props.figure.id });
                    this.env.model.dispatch("COPY");
                    await this.env.clipboard.writeText(this.env.model.getters.getClipboardContent());
                },
            });
            registry.add("cut", {
                name: _lt("Cut"),
                sequence: 3,
                action: async () => {
                    this.env.model.dispatch("SELECT_FIGURE", { id: this.props.figure.id });
                    this.env.model.dispatch("CUT");
                    await this.env.clipboard.writeText(this.env.model.getters.getClipboardContent());
                },
            });
            registry.add("delete", {
                name: _lt("Delete"),
                sequence: 10,
                action: () => {
                    this.env.model.dispatch("DELETE_FIGURE", {
                        sheetId: this.env.model.getters.getActiveSheetId(),
                        id: this.props.figure.id,
                    });
                    if (this.props.sidePanelIsOpen) {
                        this.env.toggleSidePanel("ChartPanel");
                    }
                    this.props.onFigureDeleted();
                },
            });
            return registry;
        }
        get chartType() {
            return this.env.model.getters.getChartType(this.props.figure.id);
        }
        onContextMenu(ev) {
            const position = {
                x: this.position.x + ev.offsetX,
                y: this.position.y + ev.offsetY,
            };
            this.openContextMenu(position);
        }
        showMenu() {
            const position = {
                x: this.menuButtonPosition.x - MENU_WIDTH,
                y: this.menuButtonPosition.y,
            };
            this.openContextMenu(position);
        }
        openContextMenu(position) {
            const registry = this.getMenuItemRegistry();
            this.menuState.isOpen = true;
            this.menuState.menuItems = registry.getAll();
            this.menuState.position = position;
        }
        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.template = "o-spreadsheet-ChartFigure";
    ChartFigure.components = { Menu };

    /**
     * Start listening to pointer events and apply the given callbacks.
     *
     * @returns A function to remove the listeners.
     */
    function startDnd(onMouseMove, onMouseUp, onMouseDown = () => { }) {
        const removeListeners = () => {
            window.removeEventListener("mousedown", onMouseDown);
            window.removeEventListener("mouseup", _onMouseUp);
            window.removeEventListener("dragstart", _onDragStart);
            window.removeEventListener("mousemove", onMouseMove);
            window.removeEventListener("wheel", onMouseMove);
        };
        const _onMouseUp = (ev) => {
            onMouseUp(ev);
            removeListeners();
        };
        function _onDragStart(ev) {
            ev.preventDefault();
        }
        window.addEventListener("mousedown", onMouseDown);
        window.addEventListener("mouseup", _onMouseUp);
        window.addEventListener("dragstart", _onDragStart);
        window.addEventListener("mousemove", onMouseMove);
        window.addEventListener("wheel", onMouseMove);
        return removeListeners;
    }
    /**
     * 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);
    }

    // -----------------------------------------------------------------------------
    // Autofill
    // -----------------------------------------------------------------------------
    css /* scss */ `
  .o-autofill {
    height: 6px;
    width: 6px;
    border: 1px solid white;
    position: absolute;
    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 {
        constructor() {
            super(...arguments);
            this.state = owl.useState({
                position: { left: 0, top: 0 },
                handler: false,
            });
        }
        get style() {
            const { left, top } = this.props.position;
            return `top:${top}px;left:${left}px`;
        }
        get styleHandler() {
            let position = this.state.handler ? this.state.position : { left: 0, top: 0 };
            return `top:${position.top}px;left:${position.left}px;`;
        }
        get styleNextvalue() {
            let position = this.state.handler ? this.state.position : { left: 0, top: 0 };
            return `top:${position.top + 5}px;left:${position.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;
            this.state.position = { left: 0, top: 0 };
            const { scrollY, scrollX } = this.env.model.getters.getActiveSheetScrollInfo();
            const start = {
                left: ev.clientX + scrollX,
                top: ev.clientY + scrollY,
            };
            let lastCol;
            let lastRow;
            const onMouseUp = () => {
                this.state.handler = false;
                this.env.model.dispatch("AUTOFILL");
            };
            const onMouseMove = (ev) => {
                const position = gridOverlayPosition();
                const { scrollY, scrollX } = this.env.model.getters.getActiveSheetScrollInfo();
                this.state.position = {
                    left: ev.clientX - start.left + scrollX,
                    top: ev.clientY - start.top + scrollY,
                };
                const col = this.env.model.getters.getColIndex(ev.clientX - position.left);
                const row = this.env.model.getters.getRowIndex(ev.clientY - position.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 });
                    }
                }
            };
            startDnd(onMouseMove, onMouseUp);
        }
        onDblClick() {
            this.env.model.dispatch("AUTOFILL_AUTO");
        }
    }
    Autofill.template = "o-spreadsheet-Autofill";
    class TooltipComponent extends owl.Component {
    }
    TooltipComponent.template = owl.xml /* xml */ `
    <div t-esc="props.content"/>
  `;

    css /* scss */ `
  .o-client-tag {
    position: absolute;
    border-top-left-radius: 4px;
    border-top-right-radius: 4px;
    font-size: ${DEFAULT_FONT_SIZE};
    color: white;
    opacity: 0;
    pointer-events: none;
  }
`;
    class ClientTag extends owl.Component {
        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 `bottom: ${height - y + 15}px;left: ${x - 1}px;border: 1px solid ${color};background-color: ${color};${this.props.active ? "opacity:1 !important" : ""}`;
        }
    }
    ClientTag.template = "o-spreadsheet-ClientTag";

    //------------------------------------------------------------------------------
    // Arg description DSL
    //------------------------------------------------------------------------------
    const ARG_REGEXP = /(.*?)\((.*?)\)(.*)/;
    const ARG_TYPES = [
        "ANY",
        "BOOLEAN",
        "DATE",
        "NUMBER",
        "STRING",
        "RANGE",
        "RANGE<BOOLEAN>",
        "RANGE<DATE>",
        "RANGE<NUMBER>",
        "RANGE<STRING>",
        "META",
    ];
    /**
     * This function is meant to be used as a tag for a template strings.
     *
     * Its job is to convert a textual description of the list of arguments into an
     * actual array of Arg, suitable for consumption.
     */
    function args(strings) {
        let lines = strings.split("\n");
        const result = [];
        for (let l of lines) {
            l = l.trim();
            if (l) {
                result.push(makeArg(l));
            }
        }
        return result;
    }
    function makeArg(str) {
        let parts = str.match(ARG_REGEXP);
        let name = parts[1].trim();
        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);
            }
        }
        let description = parts[3].trim();
        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);
        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(_lt("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(_lt("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(_lt("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;
        }
    }

    // HELPERS
    const SORT_TYPES_ORDER = ["number", "string", "boolean", "undefined"];
    function assert(condition, message) {
        if (!condition()) {
            throw new Error(message);
        }
    }
    // -----------------------------------------------------------------------------
    // FORMAT FUNCTIONS
    // -----------------------------------------------------------------------------
    const expectNumberValueError = (value) => _lt("The function [[FUNCTION_NAME]] expects a number value, but '%s' is a string, and cannot be coerced to a number.", value);
    function toNumber(value) {
        switch (typeof value) {
            case "number":
                return value;
            case "boolean":
                return value ? 1 : 0;
            case "string":
                if (isNumber(value) || value === "") {
                    return parseNumber(value);
                }
                const internalDate = parseDateTime(value);
                if (internalDate) {
                    return internalDate.value;
                }
                throw new Error(expectNumberValueError(value));
            default:
                return 0;
        }
    }
    function strictToNumber(value) {
        if (value === "") {
            throw new Error(expectNumberValueError(value));
        }
        return toNumber(value);
    }
    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, "");
    });
    /**
     * 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;
    }
    const expectBooleanValueError = (value) => _lt("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) {
        return numberToJsDate(toNumber(value));
    }
    // -----------------------------------------------------------------------------
    // VISIT FUNCTIONS
    // -----------------------------------------------------------------------------
    function visitArgs(args, cellCb, dataCb) {
        for (let arg of args) {
            if (Array.isArray(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) {
        visitArgs(args, (cellValue) => {
            if (typeof cellValue === "number") {
                cb(cellValue);
            }
        }, (argValue) => {
            cb(strictToNumber(argValue));
        });
    }
    // -----------------------------------------------------------------------------
    // REDUCE FUNCTIONS
    // -----------------------------------------------------------------------------
    function reduceArgs(args, cellCb, dataCb, initialValue) {
        let val = initialValue;
        for (let arg of args) {
            if (Array.isArray(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++) {
                        val = cellCb(val, arg[x][y]);
                    }
                }
            }
            else {
                // arg is set directly in the formula function
                val = dataCb(val, arg);
            }
        }
        return val;
    }
    function reduceAny(args, cb, initialValue) {
        return reduceArgs(args, cb, cb, initialValue);
    }
    function reduceNumbers(args, cb, initialValue) {
        return reduceArgs(args, (acc, ArgValue) => {
            if (typeof ArgValue === "number") {
                return cb(acc, ArgValue);
            }
            return acc;
        }, (acc, argValue) => {
            return cb(acc, strictToNumber(argValue));
        }, initialValue);
    }
    function reduceNumbersTextAs0(args, cb, initialValue) {
        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));
                }
                else {
                    return cb(acc, 0);
                }
            }
            return acc;
        }, (acc, argValue) => {
            return cb(acc, toNumber(argValue));
        }, initialValue);
    }
    // -----------------------------------------------------------------------------
    // 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 (Array.isArray(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]))
                            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) {
        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) || isDateTime(operand)) {
            operand = toNumber(operand);
        }
        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) {
        if (operand === "*") {
            return /.+/;
        }
        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 (operand === undefined) {
            return false;
        }
        if (typeof operand === "number" && operator === "=") {
            if (typeof value === "string" && (isNumber(value) || isDateTime(value))) {
                return toNumber(value) === operand;
            }
            return value === 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, isQuery = false) {
        const countArg = args.length;
        if (countArg % 2 === 1) {
            throw new Error(_lt(`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 (!Array.isArray(criteriaRange) ||
                criteriaRange.length !== dimRow ||
                criteriaRange[0].length !== dimCol) {
                throw new Error(_lt(`Function [[FUNCTION_NAME]] expects criteria_range to have the same dimension`));
            }
            const description = toString(args[i + 1]);
            predicates.push(getPredicate(description, isQuery));
        }
        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, criterion);
                    if (!validatedPredicates) {
                        break;
                    }
                }
                if (validatedPredicates) {
                    cb(i, j);
                }
            }
        }
    }
    // -----------------------------------------------------------------------------
    // COMMON FUNCTIONS
    // -----------------------------------------------------------------------------
    function getNormalizedValueFromColumnRange(range, index) {
        return normalizeValue(range[0][index]);
    }
    function getNormalizedValueFromRowRange(range, index) {
        return normalizeValue(range[index][0]);
    }
    /**
     * 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 targetType = typeof target;
        let matchVal = undefined;
        let matchValIndex = undefined;
        let indexLeft = 0;
        let indexRight = rangeLength - 1;
        let indexMedian;
        let currentIndex;
        let currentVal;
        let currentType;
        const getValue = sortOrder === "desc"
            ? (i) => getValueInData(data, rangeLength - i - 1)
            : (i) => getValueInData(data, i);
        while (indexRight - indexLeft >= 0) {
            indexMedian = Math.floor((indexLeft + indexRight) / 2);
            currentIndex = indexMedian;
            currentVal = getValue(currentIndex);
            currentType = typeof currentVal;
            // 1 - linear search to find value with the same type
            while (indexLeft < currentIndex && targetType !== currentType) {
                currentIndex--;
                currentVal = getValue(currentIndex);
                currentType = typeof currentVal;
            }
            if (currentType !== targetType || currentVal === undefined) {
                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 < currentVal ||
                    (matchVal === currentVal && matchValIndex < currentIndex)) {
                    matchVal = currentVal;
                    matchValIndex = currentIndex;
                }
            }
            else if (mode === "nextGreater" && currentVal >= target) {
                if (matchVal === undefined ||
                    matchVal > currentVal ||
                    (matchVal === currentVal && matchValIndex < currentIndex)) {
                    matchVal = currentVal;
                    matchValIndex = currentIndex;
                }
            }
            // 3 - give new indexes for the Binary search
            if (currentVal > target || (mode === "strict" && currentVal === target)) {
                indexRight = currentIndex - 1;
            }
            else {
                indexLeft = indexMedian + 1;
            }
        }
        // note that valMinIndex could be 0
        if (matchValIndex === undefined) {
            return -1;
        }
        return sortOrder === "desc" ? rangeLength - matchValIndex - 1 : matchValIndex;
    }
    /**
     * 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 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 = 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;
    }
    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;
    }

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

    var misc$1 = /*#__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;
    // -----------------------------------------------------------------------------
    // ABS
    // -----------------------------------------------------------------------------
    const ABS = {
        description: _lt("Absolute value of a number."),
        args: args(`
    value (number) ${_lt("The number of which to return the absolute value.")}
  `),
        returns: ["NUMBER"],
        compute: function (value) {
            return Math.abs(toNumber(value));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ACOS
    // -----------------------------------------------------------------------------
    const ACOS = {
        description: _lt("Inverse cosine of a value, in radians."),
        args: args(`
    value (number) ${_lt("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);
            assert(() => Math.abs(_value) <= 1, _lt("The value (%s) must be between -1 and 1 inclusive.", _value.toString()));
            return Math.acos(_value);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ACOSH
    // -----------------------------------------------------------------------------
    const ACOSH = {
        description: _lt("Inverse hyperbolic cosine of a number."),
        args: args(`
    value (number) ${_lt("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);
            assert(() => _value >= 1, _lt("The value (%s) must be greater than or equal to 1.", _value.toString()));
            return Math.acosh(_value);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ACOT
    // -----------------------------------------------------------------------------
    const ACOT = {
        description: _lt("Inverse cotangent of a value."),
        args: args(`
    value (number) ${_lt("The value for which to calculate the inverse cotangent.")}
  `),
        returns: ["NUMBER"],
        compute: function (value) {
            const _value = toNumber(value);
            const sign = Math.sign(_value) || 1;
            // ACOT has two possible configurations:
            // @compatibility Excel: return Math.PI / 2 - Math.atan(toNumber(_value));
            // @compatibility Google: return sign * Math.PI / 2 - Math.atan(toNumber(_value));
            return (sign * Math.PI) / 2 - Math.atan(_value);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ACOTH
    // -----------------------------------------------------------------------------
    const ACOTH = {
        description: _lt("Inverse hyperbolic cotangent of a value."),
        args: args(`
    value (number) ${_lt("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);
            assert(() => Math.abs(_value) > 1, _lt("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: _lt("Inverse sine of a value, in radians."),
        args: args(`
    value (number) ${_lt("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);
            assert(() => Math.abs(_value) <= 1, _lt("The value (%s) must be between -1 and 1 inclusive.", _value.toString()));
            return Math.asin(_value);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ASINH
    // -----------------------------------------------------------------------------
    const ASINH = {
        description: _lt("Inverse hyperbolic sine of a number."),
        args: args(`
    value (number) ${_lt("The value for which to calculate the inverse hyperbolic sine.")}
  `),
        returns: ["NUMBER"],
        compute: function (value) {
            return Math.asinh(toNumber(value));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ATAN
    // -----------------------------------------------------------------------------
    const ATAN = {
        description: _lt("Inverse tangent of a value, in radians."),
        args: args(`
    value (number) ${_lt("The value for which to calculate the inverse tangent.")}
  `),
        returns: ["NUMBER"],
        compute: function (value) {
            return Math.atan(toNumber(value));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ATAN2
    // -----------------------------------------------------------------------------
    const ATAN2 = {
        description: _lt("Angle from the X axis to a point (x,y), in radians."),
        args: args(`
    x (number) ${_lt("The x coordinate of the endpoint of the line segment for which to calculate the angle from the x-axis.")}
    y (number) ${_lt("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);
            const _y = toNumber(y);
            assert(() => _x !== 0 || _y !== 0, _lt(`Function [[FUNCTION_NAME]] caused a divide by zero error.`));
            return Math.atan2(_y, _x);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ATANH
    // -----------------------------------------------------------------------------
    const ATANH = {
        description: _lt("Inverse hyperbolic tangent of a number."),
        args: args(`
    value (number) ${_lt("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);
            assert(() => Math.abs(_value) < 1, _lt("The value (%s) must be between -1 and 1 exclusive.", _value.toString()));
            return Math.atanh(_value);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // CEILING
    // -----------------------------------------------------------------------------
    const CEILING = {
        description: _lt(`Rounds number up to nearest multiple of factor.`),
        args: args(`
    value (number) ${_lt("The value to round up to the nearest integer multiple of factor.")}
    factor (number, default=${DEFAULT_FACTOR}) ${_lt("The number to whose multiples value will be rounded.")}
  `),
        returns: ["NUMBER"],
        computeFormat: (value) => value === null || value === void 0 ? void 0 : value.format,
        compute: function (value, factor = DEFAULT_FACTOR) {
            const _value = toNumber(value);
            const _factor = toNumber(factor);
            assert(() => _factor >= 0 || _value <= 0, _lt("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: _lt(`Rounds number up to nearest multiple of factor.`),
        args: args(`
    number (number) ${_lt("The value to round up to the nearest integer multiple of significance.")}
    significance (number, default=${DEFAULT_SIGNIFICANCE}) ${_lt("The number to whose multiples number will be rounded. The sign of significance will be ignored.")}
    mode (number, default=${DEFAULT_MODE}) ${_lt("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 === null || number === void 0 ? void 0 : number.format,
        compute: function (number, significance = DEFAULT_SIGNIFICANCE, mode = DEFAULT_MODE) {
            let _significance = toNumber(significance);
            if (_significance === 0) {
                return 0;
            }
            const _number = toNumber(number);
            _significance = Math.abs(_significance);
            if (_number >= 0) {
                return Math.ceil(_number / _significance) * _significance;
            }
            const _mode = toNumber(mode);
            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: _lt(`Rounds number up to nearest multiple of factor.`),
        args: args(`
    number (number) ${_lt("The value to round up to the nearest integer multiple of significance.")}
    significance (number, default=${DEFAULT_SIGNIFICANCE}) ${_lt("The number to whose multiples number will be rounded.")}
  `),
        returns: ["NUMBER"],
        computeFormat: (number) => number === null || number === void 0 ? void 0 : number.format,
        compute: function (number, significance) {
            return CEILING_MATH.compute(number, significance, 0);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COS
    // -----------------------------------------------------------------------------
    const COS = {
        description: _lt("Cosine of an angle provided in radians."),
        args: args(`
    angle (number) ${_lt("The angle to find the cosine of, in radians.")}
  `),
        returns: ["NUMBER"],
        compute: function (angle) {
            return Math.cos(toNumber(angle));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COSH
    // -----------------------------------------------------------------------------
    const COSH = {
        description: _lt("Hyperbolic cosine of any real number."),
        args: args(`
    value (number) ${_lt("Any real value to calculate the hyperbolic cosine of.")}
  `),
        returns: ["NUMBER"],
        compute: function (value) {
            return Math.cosh(toNumber(value));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COT
    // -----------------------------------------------------------------------------
    const COT = {
        description: _lt("Cotangent of an angle provided in radians."),
        args: args(`
    angle (number) ${_lt("The angle to find the cotangent of, in radians.")}
  `),
        returns: ["NUMBER"],
        compute: function (angle) {
            const _angle = toNumber(angle);
            assert(() => _angle !== 0, _lt(`Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.`));
            return 1 / Math.tan(_angle);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COTH
    // -----------------------------------------------------------------------------
    const COTH = {
        description: _lt("Hyperbolic cotangent of any real number."),
        args: args(`
    value (number) ${_lt("Any real value to calculate the hyperbolic cotangent of.")}
  `),
        returns: ["NUMBER"],
        compute: function (value) {
            const _value = toNumber(value);
            assert(() => _value !== 0, _lt(`Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.`));
            return 1 / Math.tanh(_value);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COUNTBLANK
    // -----------------------------------------------------------------------------
    const COUNTBLANK = {
        description: _lt("Number of empty values."),
        args: args(`
    value1 (any, range) ${_lt("The first value or range in which to count the number of blanks.")}
    value2 (any, range, repeating) ${_lt("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: _lt("A conditional count across a range."),
        args: args(`
    range (range) ${_lt("The range that is tested against criterion.")}
    criterion (string) ${_lt("The pattern or test to apply to range.")}
  `),
        returns: ["NUMBER"],
        compute: function (...argsValues) {
            let count = 0;
            visitMatchingRanges(argsValues, (i, j) => {
                count += 1;
            });
            return count;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COUNTIFS
    // -----------------------------------------------------------------------------
    const COUNTIFS = {
        description: _lt("Count values depending on multiple criteria."),
        args: args(`
    criteria_range1 (range) ${_lt("The range to check against criterion1.")}
    criterion1 (string) ${_lt("The pattern or test to apply to criteria_range1.")}
    criteria_range2 (any, range, repeating) ${_lt("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.")}
    criterion2 (string, repeating) ${_lt("Additional criteria to check.")}
  `),
        returns: ["NUMBER"],
        compute: function (...argsValues) {
            let count = 0;
            visitMatchingRanges(argsValues, (i, j) => {
                count += 1;
            });
            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: _lt("Counts number of unique values in a range."),
        args: args(`
    value1 (any, range) ${_lt("The first value or range to consider for uniqueness.")}
    value2 (any, range, repeating) ${_lt("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: _lt("Counts number of unique values in a range, filtered by a set of criteria."),
        args: args(`
    range (range) ${_lt("The range of cells from which the number of unique values will be counted.")}
    criteria_range1 (range) ${_lt("The range of cells over which to evaluate criterion1.")}
    criterion1 (string) ${_lt("The pattern or test to apply to criteria_range1, such that each cell that evaluates to TRUE will be included in the filtered set.")}
    criteria_range2 (any, range, repeating) ${_lt("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.")}
    criterion2 (string, repeating) ${_lt("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);
                }
            });
            return uniqueValues.size;
        },
    };
    // -----------------------------------------------------------------------------
    // CSC
    // -----------------------------------------------------------------------------
    const CSC = {
        description: _lt("Cosecant of an angle provided in radians."),
        args: args(`
    angle (number) ${_lt("The angle to find the cosecant of, in radians.")}
  `),
        returns: ["NUMBER"],
        compute: function (angle) {
            const _angle = toNumber(angle);
            assert(() => _angle !== 0, _lt(`Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.`));
            return 1 / Math.sin(_angle);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // CSCH
    // -----------------------------------------------------------------------------
    const CSCH = {
        description: _lt("Hyperbolic cosecant of any real number."),
        args: args(`
    value (number) ${_lt("Any real value to calculate the hyperbolic cosecant of.")}
  `),
        returns: ["NUMBER"],
        compute: function (value) {
            const _value = toNumber(value);
            assert(() => _value !== 0, _lt(`Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.`));
            return 1 / Math.sinh(_value);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DECIMAL
    // -----------------------------------------------------------------------------
    const DECIMAL = {
        description: _lt("Converts from another base to decimal."),
        args: args(`
    value (string) ${_lt("The number to convert.")},
    base (number) ${_lt("The base to convert the value from.")},
  `),
        returns: ["NUMBER"],
        compute: function (value, base) {
            let _base = toNumber(base);
            _base = Math.floor(_base);
            assert(() => 2 <= _base && _base <= 36, _lt("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(() => !!_value.match(/^-?[a-z0-9]+$/i), _lt("The value (%s) must be a valid base %s representation.", _value, _base.toString()));
            const deci = parseInt(_value, _base);
            assert(() => !isNaN(deci), _lt("The value (%s) must be a valid base %s representation.", _value, _base.toString()));
            return deci;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DEGREES
    // -----------------------------------------------------------------------------
    const DEGREES = {
        description: _lt(`Converts an angle value in radians to degrees.`),
        args: args(`
    angle (number)  ${_lt("The angle to convert from radians to degrees.")}
  `),
        returns: ["NUMBER"],
        compute: function (angle) {
            return (toNumber(angle) * 180) / Math.PI;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // EXP
    // -----------------------------------------------------------------------------
    const EXP = {
        description: _lt(`Euler's number, e (~2.718) raised to a power.`),
        args: args(`
    value (number) ${_lt("The exponent to raise e.")}
  `),
        returns: ["NUMBER"],
        compute: function (value) {
            return Math.exp(toNumber(value));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // FLOOR
    // -----------------------------------------------------------------------------
    const FLOOR = {
        description: _lt(`Rounds number down to nearest multiple of factor.`),
        args: args(`
    value (number) ${_lt("The value to round down to the nearest integer multiple of factor.")}
    factor (number, default=${DEFAULT_FACTOR}) ${_lt("The number to whose multiples value will be rounded.")}
  `),
        returns: ["NUMBER"],
        computeFormat: (value) => value === null || value === void 0 ? void 0 : value.format,
        compute: function (value, factor = DEFAULT_FACTOR) {
            const _value = toNumber(value);
            const _factor = toNumber(factor);
            assert(() => _factor >= 0 || _value <= 0, _lt("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: _lt(`Rounds number down to nearest multiple of factor.`),
        args: args(`
    number (number) ${_lt("The value to round down to the nearest integer multiple of significance.")}
    significance (number, default=${DEFAULT_SIGNIFICANCE}) ${_lt("The number to whose multiples number will be rounded. The sign of significance will be ignored.")}
    mode (number, default=${DEFAULT_MODE}) ${_lt("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 === null || number === void 0 ? void 0 : number.format,
        compute: function (number, significance = DEFAULT_SIGNIFICANCE, mode = DEFAULT_MODE) {
            let _significance = toNumber(significance);
            if (_significance === 0) {
                return 0;
            }
            const _number = toNumber(number);
            _significance = Math.abs(_significance);
            if (_number >= 0) {
                return Math.floor(_number / _significance) * _significance;
            }
            const _mode = toNumber(mode);
            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: _lt(`Rounds number down to nearest multiple of factor.`),
        args: args(`
    number (number) ${_lt("The value to round down to the nearest integer multiple of significance.")}
    significance (number, default=${DEFAULT_SIGNIFICANCE}) ${_lt("The number to whose multiples number will be rounded.")}
  `),
        returns: ["NUMBER"],
        computeFormat: (number) => number === null || number === void 0 ? void 0 : number.format,
        compute: function (number, significance = DEFAULT_SIGNIFICANCE) {
            return FLOOR_MATH.compute(number, significance, 0);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ISEVEN
    // -----------------------------------------------------------------------------
    const ISEVEN = {
        description: _lt(`Whether the provided value is even.`),
        args: args(`
    value (number) ${_lt("The value to be verified as even.")}
  `),
        returns: ["BOOLEAN"],
        compute: function (value) {
            const _value = strictToNumber(value);
            return Math.floor(Math.abs(_value)) & 1 ? false : true;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ISO.CEILING
    // -----------------------------------------------------------------------------
    const ISO_CEILING = {
        description: _lt(`Rounds number up to nearest multiple of factor.`),
        args: args(`
      number (number) ${_lt("The value to round up to the nearest integer multiple of significance.")}
      significance (number, default=${DEFAULT_SIGNIFICANCE}) ${_lt("The number to whose multiples number will be rounded.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (number) => number === null || number === void 0 ? void 0 : number.format,
        compute: function (number, significance = DEFAULT_SIGNIFICANCE) {
            return CEILING_MATH.compute(number, significance, 0);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ISODD
    // -----------------------------------------------------------------------------
    const ISODD = {
        description: _lt(`Whether the provided value is even.`),
        args: args(`
    value (number) ${_lt("The value to be verified as even.")}
  `),
        returns: ["BOOLEAN"],
        compute: function (value) {
            const _value = strictToNumber(value);
            return Math.floor(Math.abs(_value)) & 1 ? true : false;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // LN
    // -----------------------------------------------------------------------------
    const LN = {
        description: _lt(`The logarithm of a number, base e (euler's number).`),
        args: args(`
    value (number) ${_lt("The value for which to calculate the logarithm, base e.")}
  `),
        returns: ["NUMBER"],
        compute: function (value) {
            const _value = toNumber(value);
            assert(() => _value > 0, _lt("The value (%s) must be strictly positive.", _value.toString()));
            return Math.log(_value);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // LOG
    // -----------------------------------------------------------------------------
    const LOG = {
        description: _lt("The logarithm of a number, for a given base."),
        args: args(`
    value (number) ${_lt("The value for which to calculate the logarithm, base e.")}
    base (number, default=10) ${_lt("The base of the logarithm.")}
  `),
        returns: ["NUMBER"],
        compute: function (value, base = 10) {
            const _value = toNumber(value);
            const _base = toNumber(base);
            assert(() => _value > 0, _lt("The value (%s) must be strictly positive.", _value.toString()));
            assert(() => _base > 0, _lt("The base (%s) must be strictly positive.", _base.toString()));
            assert(() => _base !== 1, _lt("The base must be different from 1."));
            return Math.log10(_value) / Math.log10(_base);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // MOD
    // -----------------------------------------------------------------------------
    const MOD = {
        description: _lt(`Modulo (remainder) operator.`),
        args: args(`
      dividend (number) ${_lt("The number to be divided to find the remainder.")}
      divisor (number) ${_lt("The number to divide by.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (dividend) => dividend === null || dividend === void 0 ? void 0 : dividend.format,
        compute: function (dividend, divisor) {
            const _divisor = toNumber(divisor);
            assert(() => _divisor !== 0, _lt("The divisor must be different from 0."));
            const _dividend = toNumber(dividend);
            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,
    };
    // -----------------------------------------------------------------------------
    // ODD
    // -----------------------------------------------------------------------------
    const ODD = {
        description: _lt(`Rounds a number up to the nearest odd integer.`),
        args: args(`
      value (number) ${_lt("The value to round to the next greatest odd number.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (number) => number === null || number === void 0 ? void 0 : number.format,
        compute: function (value) {
            const _value = toNumber(value);
            let temp = Math.ceil(Math.abs(_value));
            temp = temp & 1 ? temp : temp + 1;
            return _value < 0 ? -temp : temp;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // PI
    // -----------------------------------------------------------------------------
    const PI = {
        description: _lt(`The number pi.`),
        args: [],
        returns: ["NUMBER"],
        compute: function () {
            return Math.PI;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // POWER
    // -----------------------------------------------------------------------------
    const POWER = {
        description: _lt(`A number raised to a power.`),
        args: args(`
      base (number) ${_lt("The number to raise to the exponent power.")}
      exponent (number) ${_lt("The exponent to raise base to.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (base) => base === null || base === void 0 ? void 0 : base.format,
        compute: function (base, exponent) {
            const _base = toNumber(base);
            const _exponent = toNumber(exponent);
            assert(() => _base >= 0 || Number.isInteger(_exponent), _lt("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: _lt("Result of multiplying a series of numbers together."),
        args: args(`
      factor1 (number, range<number>) ${_lt("The first number or range to calculate for the product.")}
      factor2 (number, range<number>, repeating) ${_lt("More numbers or ranges to calculate for the product.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (factor1) => {
            var _a;
            return Array.isArray(factor1) ? (_a = factor1[0][0]) === null || _a === void 0 ? void 0 : _a.format : factor1 === null || factor1 === void 0 ? void 0 : factor1.format;
        },
        compute: function (...factors) {
            let count = 0;
            let acc = 1;
            for (let n of factors) {
                if (Array.isArray(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);
                    count += 1;
                }
            }
            if (count === 0) {
                return 0;
            }
            return acc;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // RAND
    // -----------------------------------------------------------------------------
    const RAND = {
        description: _lt("A random number between 0 inclusive and 1 exclusive."),
        args: [],
        returns: ["NUMBER"],
        compute: function () {
            return Math.random();
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // RANDBETWEEN
    // -----------------------------------------------------------------------------
    const RANDBETWEEN = {
        description: _lt("Random integer between two values, inclusive."),
        args: args(`
      low (number) ${_lt("The low end of the random range.")}
      high (number) ${_lt("The high end of the random range.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (low) => low === null || low === void 0 ? void 0 : low.format,
        compute: function (low, high) {
            let _low = toNumber(low);
            if (!Number.isInteger(_low)) {
                _low = Math.ceil(_low);
            }
            let _high = toNumber(high);
            if (!Number.isInteger(_high)) {
                _high = Math.floor(_high);
            }
            assert(() => _low <= _high, _lt("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: _lt("Rounds a number according to standard rules."),
        args: args(`
      value (number) ${_lt("The value to round to places number of places.")}
      places (number, default=${DEFAULT_PLACES}) ${_lt("The number of decimal places to which to round.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (value) => value === null || value === void 0 ? void 0 : value.format,
        compute: function (value, places = DEFAULT_PLACES) {
            const _value = toNumber(value);
            let _places = toNumber(places);
            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: _lt(`Rounds down a number.`),
        args: args(`
      value (number) ${_lt("The value to round to places number of places, always rounding down.")}
      places (number, default=${DEFAULT_PLACES}) ${_lt("The number of decimal places to which to round.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (value) => value === null || value === void 0 ? void 0 : value.format,
        compute: function (value, places = DEFAULT_PLACES) {
            const _value = toNumber(value);
            let _places = toNumber(places);
            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: _lt(`Rounds up a number.`),
        args: args(`
      value (number) ${_lt("The value to round to places number of places, always rounding up.")}
      places (number, default=${DEFAULT_PLACES}) ${_lt("The number of decimal places to which to round.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (value) => value === null || value === void 0 ? void 0 : value.format,
        compute: function (value, places = DEFAULT_PLACES) {
            const _value = toNumber(value);
            let _places = toNumber(places);
            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: _lt("Secant of an angle provided in radians."),
        args: args(`
    angle (number) ${_lt("The angle to find the secant of, in radians.")}
  `),
        returns: ["NUMBER"],
        compute: function (angle) {
            return 1 / Math.cos(toNumber(angle));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SECH
    // -----------------------------------------------------------------------------
    const SECH = {
        description: _lt("Hyperbolic secant of any real number."),
        args: args(`
    value (number) ${_lt("Any real value to calculate the hyperbolic secant of.")}
  `),
        returns: ["NUMBER"],
        compute: function (value) {
            return 1 / Math.cosh(toNumber(value));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SIN
    // -----------------------------------------------------------------------------
    const SIN = {
        description: _lt("Sine of an angle provided in radians."),
        args: args(`
      angle (number) ${_lt("The angle to find the sine of, in radians.")}
    `),
        returns: ["NUMBER"],
        compute: function (angle) {
            return Math.sin(toNumber(angle));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SINH
    // -----------------------------------------------------------------------------
    const SINH = {
        description: _lt("Hyperbolic sine of any real number."),
        args: args(`
    value (number) ${_lt("Any real value to calculate the hyperbolic sine of.")}
  `),
        returns: ["NUMBER"],
        compute: function (value) {
            return Math.sinh(toNumber(value));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SQRT
    // -----------------------------------------------------------------------------
    const SQRT = {
        description: _lt("Positive square root of a positive number."),
        args: args(`
      value (number) ${_lt("The number for which to calculate the positive square root.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (value) => value === null || value === void 0 ? void 0 : value.format,
        compute: function (value) {
            const _value = toNumber(value);
            assert(() => _value >= 0, _lt("The value (%s) must be positive or null.", _value.toString()));
            return Math.sqrt(_value);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SUM
    // -----------------------------------------------------------------------------
    const SUM = {
        description: _lt("Sum of a series of numbers and/or cells."),
        args: args(`
      value1 (number, range<number>) ${_lt("The first number or range to add together.")}
      value2 (number, range<number>, repeating) ${_lt("Additional numbers or ranges to add to value1.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (value1) => {
            var _a;
            return Array.isArray(value1) ? (_a = value1[0][0]) === null || _a === void 0 ? void 0 : _a.format : value1 === null || value1 === void 0 ? void 0 : value1.format;
        },
        compute: function (...values) {
            return reduceNumbers(values, (acc, a) => acc + a, 0);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SUMIF
    // -----------------------------------------------------------------------------
    const SUMIF = {
        description: _lt("A conditional sum across a range."),
        args: args(`
      criteria_range (range) ${_lt("The range which is tested against criterion.")}
      criterion (string) ${_lt("The pattern or test to apply to range.")}
      sum_range (range, default=criteria_range) ${_lt("The range to be summed, if different from range.")}
    `),
        returns: ["NUMBER"],
        compute: function (criteriaRange, criterion, sumRange = undefined) {
            if (sumRange === undefined) {
                sumRange = criteriaRange;
            }
            let sum = 0;
            visitMatchingRanges([criteriaRange, criterion], (i, j) => {
                const value = sumRange[i][j];
                if (typeof value === "number") {
                    sum += value;
                }
            });
            return sum;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SUMIFS
    // -----------------------------------------------------------------------------
    const SUMIFS = {
        description: _lt("Sums a range depending on multiple criteria."),
        args: args(`
      sum_range (range) ${_lt("The range to sum.")}
      criteria_range1 (range) ${_lt("The range to check against criterion1.")}
      criterion1 (string) ${_lt("The pattern or test to apply to criteria_range1.")}
      criteria_range2 (any, range, repeating) ${_lt("Additional ranges to check.")}
      criterion2 (string, repeating) ${_lt("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;
                }
            });
            return sum;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // TAN
    // -----------------------------------------------------------------------------
    const TAN = {
        description: _lt("Tangent of an angle provided in radians."),
        args: args(`
    angle (number) ${_lt("The angle to find the tangent of, in radians.")}
  `),
        returns: ["NUMBER"],
        compute: function (angle) {
            return Math.tan(toNumber(angle));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // TANH
    // -----------------------------------------------------------------------------
    const TANH = {
        description: _lt("Hyperbolic tangent of any real number."),
        args: args(`
    value (number) ${_lt("Any real value to calculate the hyperbolic tangent of.")}
  `),
        returns: ["NUMBER"],
        compute: function (value) {
            return Math.tanh(toNumber(value));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // TRUNC
    // -----------------------------------------------------------------------------
    const TRUNC = {
        description: _lt("Truncates a number."),
        args: args(`
      value (number) ${_lt("The value to be truncated.")}
      places (number, default=${DEFAULT_PLACES}) ${_lt("The number of significant digits to the right of the decimal point to retain.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (value) => value === null || value === void 0 ? void 0 : value.format,
        compute: function (value, places = DEFAULT_PLACES) {
            const _value = toNumber(value);
            let _places = toNumber(places);
            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,
        ISO_CEILING: ISO_CEILING,
        ISODD: ISODD,
        LN: LN,
        LOG: LOG,
        MOD: MOD,
        ODD: ODD,
        PI: PI,
        POWER: POWER,
        PRODUCT: PRODUCT,
        RAND: RAND,
        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
    });

    // Note: dataY and dataX may not have the same dimension
    function covariance(dataY, dataX, isSample) {
        let flatDataY = [];
        let 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, _lt("[[FUNCTION_NAME]] has mismatched argument count %s vs %s.", lenY.toString(), lenX.toString()));
        let count = 0;
        let sumY = 0;
        let sumX = 0;
        for (let i = 0; i < lenY; i++) {
            const valueY = flatDataY[i];
            const valueX = flatDataX[i];
            if (typeof valueY === "number" && typeof valueX === "number") {
                count += 1;
                sumY += valueY;
                sumX += valueX;
            }
        }
        assert(() => count !== 0 && (!isSample || count !== 1), _lt(`Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.`));
        const averageY = sumY / count;
        const averageX = sumX / count;
        let acc = 0;
        for (let i = 0; i < lenY; i++) {
            const valueY = flatDataY[i];
            const valueX = flatDataX[i];
            if (typeof valueY === "number" && typeof valueX === "number") {
                acc += (valueY - averageY) * (valueX - averageX);
            }
        }
        return acc / (count - (isSample ? 1 : 0));
    }
    function variance(args, isSample, textAs0) {
        let count = 0;
        let sum = 0;
        const reduceFunction = textAs0 ? reduceNumbersTextAs0 : reduceNumbers;
        sum = reduceFunction(args, (acc, a) => {
            count += 1;
            return acc + a;
        }, 0);
        assert(() => count !== 0 && (!isSample || count !== 1), _lt(`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) /
            (count - (isSample ? 1 : 0)));
    }
    function centile(data, percent, isInclusive) {
        const _percent = toNumber(percent);
        assert(() => (isInclusive ? 0 <= _percent && _percent <= 1 : 0 < _percent && _percent < 1), _lt(`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, _lt(`[[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), _lt(`Function [[FUNCTION_NAME]] parameter 2 value is out of range.`));
        }
        return percentile(sortedArray, _percent, isInclusive);
    }
    // -----------------------------------------------------------------------------
    // AVEDEV
    // -----------------------------------------------------------------------------
    const AVEDEV = {
        description: _lt("Average magnitude of deviations from mean."),
        args: args(`
    value1 (number, range<number>) ${_lt("The first value or range of the sample.")}
    value2 (number, range<number>, repeating) ${_lt("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);
            assert(() => count !== 0, _lt(`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) / count;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // AVERAGE
    // -----------------------------------------------------------------------------
    const AVERAGE = {
        description: _lt(`Numerical average value in a dataset, ignoring text.`),
        args: args(`
      value1 (number, range<number>) ${_lt("The first value or range to consider when calculating the average value.")}
      value2 (number, range<number>, repeating) ${_lt("Additional values or ranges to consider when calculating the average value.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (value1) => {
            var _a;
            return Array.isArray(value1) ? (_a = value1[0][0]) === null || _a === void 0 ? void 0 : _a.format : value1 === null || value1 === void 0 ? void 0 : value1.format;
        },
        compute: function (...values) {
            let count = 0;
            const sum = reduceNumbers(values, (acc, a) => {
                count += 1;
                return acc + a;
            }, 0);
            assert(() => count !== 0, _lt(`Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.`));
            return sum / count;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // AVERAGE.WEIGHTED
    // -----------------------------------------------------------------------------
    const rangeError = _lt(`[[FUNCTION_NAME]] has mismatched range sizes.`);
    const negativeWeightError = _lt(`[[FUNCTION_NAME]] expects the weight to be positive or equal to 0.`);
    const AVERAGE_WEIGHTED = {
        description: _lt(`Weighted average.`),
        args: args(`
      values (number, range<number>) ${_lt("Values to average.")}
      weights (number, range<number>) ${_lt("Weights for each corresponding value.")}
      additional_values (number, range<number>, repeating) ${_lt("Additional values to average.")}
      additional_weights (number, range<number>, repeating) ${_lt("Additional weights.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (values) => {
            var _a;
            return Array.isArray(values) ? (_a = values[0][0]) === null || _a === void 0 ? void 0 : _a.format : values === null || values === void 0 ? void 0 : values.format;
        },
        compute: function (...values) {
            let sum = 0;
            let count = 0;
            let value;
            let weight;
            assert(() => values.length % 2 === 0, _lt(`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 (Array.isArray(value)) {
                    assert(() => Array.isArray(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, _lt(`[[FUNCTION_NAME]] expects number values.`));
                            if (subWeightIsNumber) {
                                assert(() => subWeight >= 0, negativeWeightError);
                                sum += subValue * subWeight;
                                count += subWeight;
                            }
                        }
                    }
                }
                else {
                    weight = toNumber(weight);
                    value = toNumber(value);
                    assert(() => weight >= 0, negativeWeightError);
                    sum += value * weight;
                    count += weight;
                }
            }
            assert(() => count !== 0, _lt(`Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.`));
            return sum / count;
        },
    };
    // -----------------------------------------------------------------------------
    // AVERAGEA
    // -----------------------------------------------------------------------------
    const AVERAGEA = {
        description: _lt(`Numerical average value in a dataset.`),
        args: args(`
      value1 (number, range<number>) ${_lt("The first value or range to consider when calculating the average value.")}
      value2 (number, range<number>, repeating) ${_lt("Additional values or ranges to consider when calculating the average value.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (value1) => {
            var _a;
            return Array.isArray(value1) ? (_a = value1[0][0]) === null || _a === void 0 ? void 0 : _a.format : value1 === null || value1 === void 0 ? void 0 : value1.format;
        },
        compute: function (...values) {
            let count = 0;
            const sum = reduceNumbersTextAs0(values, (acc, a) => {
                count += 1;
                return acc + a;
            }, 0);
            assert(() => count !== 0, _lt(`Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.`));
            return sum / count;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // AVERAGEIF
    // -----------------------------------------------------------------------------
    const AVERAGEIF = {
        description: _lt(`Average of values depending on criteria.`),
        args: args(`
      criteria_range (range) ${_lt("The range to check against criterion.")}
      criterion (string) ${_lt("The pattern or test to apply to criteria_range.")}
      average_range (range, default=criteria_range) ${_lt("The range to average. If not included, criteria_range is used for the average instead.")}
    `),
        returns: ["NUMBER"],
        compute: function (criteriaRange, criterion, averageRange) {
            if (averageRange === undefined || averageRange === null) {
                averageRange = criteriaRange;
            }
            let count = 0;
            let sum = 0;
            visitMatchingRanges([criteriaRange, criterion], (i, j) => {
                const value = (averageRange || criteriaRange)[i][j];
                if (typeof value === "number") {
                    count += 1;
                    sum += value;
                }
            });
            assert(() => count !== 0, _lt(`Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.`));
            return sum / count;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // AVERAGEIFS
    // -----------------------------------------------------------------------------
    const AVERAGEIFS = {
        description: _lt(`Average of values depending on multiple criteria.`),
        args: args(`
      average_range (range) ${_lt("The range to average.")}
      criteria_range1 (range) ${_lt("The range to check against criterion1.")}
      criterion1 (string) ${_lt("The pattern or test to apply to criteria_range1.")}
      criteria_range2 (any, range, repeating) ${_lt("Additional criteria_range and criterion to check.")}
      criterion2 (string, repeating) ${_lt("The pattern or test to apply to criteria_range2.")}
    `),
        returns: ["NUMBER"],
        compute: function (averageRange, ...values) {
            let count = 0;
            let sum = 0;
            visitMatchingRanges(values, (i, j) => {
                const value = averageRange[i][j];
                if (typeof value === "number") {
                    count += 1;
                    sum += value;
                }
            });
            assert(() => count !== 0, _lt(`Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.`));
            return sum / count;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COUNT
    // -----------------------------------------------------------------------------
    const COUNT = {
        description: _lt(`The number of numeric values in dataset.`),
        args: args(`
    value1 (number, range<number>) ${_lt("The first value or range to consider when counting.")}
    value2 (number, range<number>, repeating) ${_lt("Additional values or ranges to consider when counting.")}
  `),
        returns: ["NUMBER"],
        compute: function (...values) {
            let count = 0;
            for (let n of values) {
                if (Array.isArray(n)) {
                    for (let i of n) {
                        for (let j of i) {
                            if (typeof j === "number") {
                                count += 1;
                            }
                        }
                    }
                }
                else if (typeof n !== "string" || isNumber(n) || parseDateTime(n)) {
                    count += 1;
                }
            }
            return count;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COUNTA
    // -----------------------------------------------------------------------------
    const COUNTA = {
        description: _lt(`The number of values in a dataset.`),
        args: args(`
    value1 (any, range) ${_lt("The first value or range to consider when counting.")}
    value2 (any, range, repeating) ${_lt("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: _lt(`The covariance of a dataset.`),
        args: args(`
    data_y (any, range) ${_lt("The range representing the array or matrix of dependent data.")}
    data_x (any, range) ${_lt("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: _lt(`The covariance of a dataset.`),
        args: args(`
    data_y (any, range) ${_lt("The range representing the array or matrix of dependent data.")}
    data_x (any, range) ${_lt("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: _lt(`The sample covariance of a dataset.`),
        args: args(`
    data_y (any, range) ${_lt("The range representing the array or matrix of dependent data.")}
    data_x (any, range) ${_lt("The range representing the array or matrix of independent data.")}
  `),
        returns: ["NUMBER"],
        compute: function (dataY, dataX) {
            return covariance(dataY, dataX, true);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // LARGE
    // -----------------------------------------------------------------------------
    const LARGE = {
        description: _lt("Nth largest element from a data set."),
        args: args(`
      data (any, range) ${_lt("Array or range containing the dataset to consider.")}
      n (number) ${_lt("The rank from largest to smallest of the element to return.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (data) => {
            var _a;
            return Array.isArray(data) ? (_a = data[0][0]) === null || _a === void 0 ? void 0 : _a.format : data === null || data === void 0 ? void 0 : data.format;
        },
        compute: function (data, n) {
            const _n = Math.trunc(toNumber(n));
            let largests = [];
            let index;
            let count = 0;
            visitAny([data], (d) => {
                if (typeof d === "number") {
                    index = dichotomicSearch(largests, d, "nextSmaller", "asc", largests.length, (array, i) => array[i]);
                    largests.splice(index + 1, 0, d);
                    count++;
                    if (count > _n) {
                        largests.shift();
                        count--;
                    }
                }
            });
            const result = largests.shift();
            assert(() => result !== undefined, _lt(`[[FUNCTION_NAME]] has no valid input data.`));
            assert(() => count >= _n, _lt("Function [[FUNCTION_NAME]] parameter 2 value (%s) is out of range.", _n.toString()));
            return result;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // MAX
    // -----------------------------------------------------------------------------
    const MAX = {
        description: _lt("Maximum value in a numeric dataset."),
        args: args(`
      value1 (number, range<number>) ${_lt("The first value or range to consider when calculating the maximum value.")}
      value2 (number, range<number>, repeating) ${_lt("Additional values or ranges to consider when calculating the maximum value.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (value1) => {
            var _a;
            return Array.isArray(value1) ? (_a = value1[0][0]) === null || _a === void 0 ? void 0 : _a.format : value1 === null || value1 === void 0 ? void 0 : value1.format;
        },
        compute: function (...values) {
            const result = reduceNumbers(values, (acc, a) => (acc < a ? a : acc), -Infinity);
            return result === -Infinity ? 0 : result;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // MAXA
    // -----------------------------------------------------------------------------
    const MAXA = {
        description: _lt("Maximum numeric value in a dataset."),
        args: args(`
      value1 (any, range) ${_lt("The first value or range to consider when calculating the maximum value.")}
      value2 (any, range, repeating) ${_lt("Additional values or ranges to consider when calculating the maximum value.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (value1) => {
            var _a;
            return Array.isArray(value1) ? (_a = value1[0][0]) === null || _a === void 0 ? void 0 : _a.format : value1 === null || value1 === void 0 ? void 0 : value1.format;
        },
        compute: function (...values) {
            const maxa = reduceNumbersTextAs0(values, (acc, a) => {
                return Math.max(a, acc);
            }, -Infinity);
            return maxa === -Infinity ? 0 : maxa;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // MAXIFS
    // -----------------------------------------------------------------------------
    const MAXIFS = {
        description: _lt("Returns the maximum value in a range of cells, filtered by a set of criteria."),
        args: args(`
      range (range) ${_lt("The range of cells from which the maximum will be determined.")}
      criteria_range1 (range) ${_lt("The range of cells over which to evaluate criterion1.")}
      criterion1 (string) ${_lt("The pattern or test to apply to criteria_range1, such that each cell that evaluates to TRUE will be included in the filtered set.")}
      criteria_range2 (any, range, repeating) ${_lt("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.")}
      criterion2 (string, repeating) ${_lt("The pattern or test to apply to criteria_range2.")}
    `),
        returns: ["NUMBER"],
        compute: function (range, ...args) {
            let result = -Infinity;
            visitMatchingRanges(args, (i, j) => {
                const value = range[i][j];
                if (typeof value === "number") {
                    result = result < value ? value : result;
                }
            });
            return result === -Infinity ? 0 : result;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // MEDIAN
    // -----------------------------------------------------------------------------
    const MEDIAN = {
        description: _lt("Median value in a numeric dataset."),
        args: args(`
      value1 (any, range) ${_lt("The first value or range to consider when calculating the median value.")}
      value2 (any, range, repeating) ${_lt("Additional values or ranges to consider when calculating the median value.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (value1) => {
            var _a;
            return Array.isArray(value1) ? (_a = value1[0][0]) === null || _a === void 0 ? void 0 : _a.format : value1 === null || value1 === void 0 ? void 0 : value1.format;
        },
        compute: function (...values) {
            let data = [];
            visitNumbers(values, (arg) => {
                data.push(arg);
            });
            return centile(data, 0.5, true);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // MIN
    // -----------------------------------------------------------------------------
    const MIN = {
        description: _lt("Minimum value in a numeric dataset."),
        args: args(`
      value1 (number, range<number>) ${_lt("The first value or range to consider when calculating the minimum value.")}
      value2 (number, range<number>, repeating) ${_lt("Additional values or ranges to consider when calculating the minimum value.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (value1) => {
            var _a;
            return Array.isArray(value1) ? (_a = value1[0][0]) === null || _a === void 0 ? void 0 : _a.format : value1 === null || value1 === void 0 ? void 0 : value1.format;
        },
        compute: function (...values) {
            const result = reduceNumbers(values, (acc, a) => (a < acc ? a : acc), Infinity);
            return result === Infinity ? 0 : result;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // MINA
    // -----------------------------------------------------------------------------
    const MINA = {
        description: _lt("Minimum numeric value in a dataset."),
        args: args(`
      value1 (number, range<number>) ${_lt("The first value or range to consider when calculating the minimum value.")}
      value2 (number, range<number>, repeating) ${_lt("Additional values or ranges to consider when calculating the minimum value.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (value1) => {
            var _a;
            return Array.isArray(value1) ? (_a = value1[0][0]) === null || _a === void 0 ? void 0 : _a.format : value1 === null || value1 === void 0 ? void 0 : value1.format;
        },
        compute: function (...values) {
            const mina = reduceNumbersTextAs0(values, (acc, a) => {
                return Math.min(a, acc);
            }, Infinity);
            return mina === Infinity ? 0 : mina;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // MINIFS
    // -----------------------------------------------------------------------------
    const MINIFS = {
        description: _lt("Returns the minimum value in a range of cells, filtered by a set of criteria."),
        args: args(`
      range (range) ${_lt("The range of cells from which the minimum will be determined.")}
      criteria_range1 (range) ${_lt("The range of cells over which to evaluate criterion1.")}
      criterion1 (string) ${_lt("The pattern or test to apply to criteria_range1, such that each cell that evaluates to TRUE will be included in the filtered set.")}
      criteria_range2 (any, range, repeating) ${_lt("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.")}
      criterion2 (string, repeating) ${_lt("The pattern or test to apply to criteria_range2.")}
    `),
        returns: ["NUMBER"],
        compute: function (range, ...args) {
            let result = Infinity;
            visitMatchingRanges(args, (i, j) => {
                const value = range[i][j];
                if (typeof value === "number") {
                    result = result > value ? value : result;
                }
            });
            return result === Infinity ? 0 : result;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // PERCENTILE
    // -----------------------------------------------------------------------------
    const PERCENTILE = {
        description: _lt("Value at a given percentile of a dataset."),
        args: args(`
      data (any, range) ${_lt("The array or range containing the dataset to consider.")}
      percentile (number) ${_lt("The percentile whose value within data will be calculated and returned.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (data) => {
            var _a;
            return Array.isArray(data) ? (_a = data[0][0]) === null || _a === void 0 ? void 0 : _a.format : data === null || data === void 0 ? void 0 : data.format;
        },
        compute: function (data, percentile) {
            return PERCENTILE_INC.compute(data, percentile);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // PERCENTILE.EXC
    // -----------------------------------------------------------------------------
    const PERCENTILE_EXC = {
        description: _lt("Value at a given percentile of a dataset exclusive of 0 and 1."),
        args: args(`
      data (any, range) ${_lt("The array or range containing the dataset to consider.")}
      percentile (number) ${_lt("The percentile, exclusive of 0 and 1, whose value within 'data' will be calculated and returned.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (data) => {
            var _a;
            return Array.isArray(data) ? (_a = data[0][0]) === null || _a === void 0 ? void 0 : _a.format : data === null || data === void 0 ? void 0 : data.format;
        },
        compute: function (data, percentile) {
            return centile([data], percentile, false);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // PERCENTILE.INC
    // -----------------------------------------------------------------------------
    const PERCENTILE_INC = {
        description: _lt("Value at a given percentile of a dataset."),
        args: args(`
      data (any, range) ${_lt("The array or range containing the dataset to consider.")}
      percentile (number) ${_lt("The percentile whose value within data will be calculated and returned.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (data) => {
            var _a;
            return Array.isArray(data) ? (_a = data[0][0]) === null || _a === void 0 ? void 0 : _a.format : data === null || data === void 0 ? void 0 : data.format;
        },
        compute: function (data, percentile) {
            return centile([data], percentile, true);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // QUARTILE
    // -----------------------------------------------------------------------------
    const QUARTILE = {
        description: _lt("Value nearest to a specific quartile of a dataset."),
        args: args(`
      data (any, range) ${_lt("The array or range containing the dataset to consider.")}
      quartile_number (number) ${_lt("Which quartile value to return.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (data) => {
            var _a;
            return Array.isArray(data) ? (_a = data[0][0]) === null || _a === void 0 ? void 0 : _a.format : data === null || data === void 0 ? void 0 : data.format;
        },
        compute: function (data, quartileNumber) {
            return QUARTILE_INC.compute(data, quartileNumber);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // QUARTILE.EXC
    // -----------------------------------------------------------------------------
    const QUARTILE_EXC = {
        description: _lt("Value nearest to a specific quartile of a dataset exclusive of 0 and 4."),
        args: args(`
      data (any, range) ${_lt("The array or range containing the dataset to consider.")}
      quartile_number (number) ${_lt("Which quartile value, exclusive of 0 and 4, to return.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (data) => {
            var _a;
            return Array.isArray(data) ? (_a = data[0][0]) === null || _a === void 0 ? void 0 : _a.format : data === null || data === void 0 ? void 0 : data.format;
        },
        compute: function (data, quartileNumber) {
            const _quartileNumber = Math.trunc(toNumber(quartileNumber));
            return centile([data], 0.25 * _quartileNumber, false);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // QUARTILE.INC
    // -----------------------------------------------------------------------------
    const QUARTILE_INC = {
        description: _lt("Value nearest to a specific quartile of a dataset."),
        args: args(`
      data (any, range) ${_lt("The array or range containing the dataset to consider.")}
      quartile_number (number) ${_lt("Which quartile value to return.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (data) => {
            var _a;
            return Array.isArray(data) ? (_a = data[0][0]) === null || _a === void 0 ? void 0 : _a.format : data === null || data === void 0 ? void 0 : data.format;
        },
        compute: function (data, quartileNumber) {
            const _quartileNumber = Math.trunc(toNumber(quartileNumber));
            return centile([data], 0.25 * _quartileNumber, true);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SMALL
    // -----------------------------------------------------------------------------
    const SMALL = {
        description: _lt("Nth smallest element in a data set."),
        args: args(`
      data (any, range) ${_lt("The array or range containing the dataset to consider.")}
      n (number) ${_lt("The rank from smallest to largest of the element to return.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (data) => {
            var _a;
            return Array.isArray(data) ? (_a = data[0][0]) === null || _a === void 0 ? void 0 : _a.format : data === null || data === void 0 ? void 0 : data.format;
        },
        compute: function (data, n) {
            const _n = Math.trunc(toNumber(n));
            let largests = [];
            let index;
            let count = 0;
            visitAny([data], (d) => {
                if (typeof d === "number") {
                    index = dichotomicSearch(largests, d, "nextSmaller", "asc", largests.length, (array, i) => array[i]);
                    largests.splice(index + 1, 0, d);
                    count++;
                    if (count > _n) {
                        largests.pop();
                        count--;
                    }
                }
            });
            const result = largests.pop();
            assert(() => result !== undefined, _lt(`[[FUNCTION_NAME]] has no valid input data.`));
            assert(() => count >= _n, _lt("Function [[FUNCTION_NAME]] parameter 2 value (%s) is out of range.", _n.toString()));
            return result;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // STDEV
    // -----------------------------------------------------------------------------
    const STDEV = {
        description: _lt("Standard deviation."),
        args: args(`
      value1 (number, range<number>) ${_lt("The first value or range of the sample.")}
      value2 (number, range<number>, repeating) ${_lt("Additional values or ranges to include in the sample.")}
    `),
        returns: ["NUMBER"],
        compute: function (...values) {
            return Math.sqrt(VAR.compute(...values));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // STDEV.P
    // -----------------------------------------------------------------------------
    const STDEV_P = {
        description: _lt("Standard deviation of entire population."),
        args: args(`
      value1 (number, range<number>) ${_lt("The first value or range of the population.")}
      value2 (number, range<number>, repeating) ${_lt("Additional values or ranges to include in the population.")}
    `),
        returns: ["NUMBER"],
        compute: function (...values) {
            return Math.sqrt(VAR_P.compute(...values));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // STDEV.S
    // -----------------------------------------------------------------------------
    const STDEV_S = {
        description: _lt("Standard deviation."),
        args: args(`
      value1 (number, range<number>) ${_lt("The first value or range of the sample.")}
      value2 (number, range<number>, repeating) ${_lt("Additional values or ranges to include in the sample.")}
    `),
        returns: ["NUMBER"],
        compute: function (...values) {
            return Math.sqrt(VAR_S.compute(...values));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // STDEVA
    // -----------------------------------------------------------------------------
    const STDEVA = {
        description: _lt("Standard deviation of sample (text as 0)."),
        args: args(`
    value1 (number, range<number>) ${_lt("The first value or range of the sample.")}
    value2 (number, range<number>, repeating) ${_lt("Additional values or ranges to include in the sample.")}
  `),
        returns: ["NUMBER"],
        compute: function (...values) {
            return Math.sqrt(VARA.compute(...values));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // STDEVP
    // -----------------------------------------------------------------------------
    const STDEVP = {
        description: _lt("Standard deviation of entire population."),
        args: args(`
    value1 (number, range<number>) ${_lt("The first value or range of the population.")}
    value2 (number, range<number>, repeating) ${_lt("Additional values or ranges to include in the population.")}
  `),
        returns: ["NUMBER"],
        compute: function (...values) {
            return Math.sqrt(VARP.compute(...values));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // STDEVPA
    // -----------------------------------------------------------------------------
    const STDEVPA = {
        description: _lt("Standard deviation of entire population (text as 0)."),
        args: args(`
    value1 (number, range<number>) ${_lt("The first value or range of the population.")}
    value2 (number, range<number>, repeating) ${_lt("Additional values or ranges to include in the population.")}
  `),
        returns: ["NUMBER"],
        compute: function (...values) {
            return Math.sqrt(VARPA.compute(...values));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // VAR
    // -----------------------------------------------------------------------------
    const VAR = {
        description: _lt("Variance."),
        args: args(`
      value1 (number, range<number>) ${_lt("The first value or range of the sample.")}
      value2 (number, range<number>, repeating) ${_lt("Additional values or ranges to include in the sample.")}
    `),
        returns: ["NUMBER"],
        compute: function (...values) {
            return variance(values, true, false);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // VAR.P
    // -----------------------------------------------------------------------------
    const VAR_P = {
        description: _lt("Variance of entire population."),
        args: args(`
      value1 (number, range<number>) ${_lt("The first value or range of the population.")}
      value2 (number, range<number>, repeating) ${_lt("Additional values or ranges to include in the population.")}
    `),
        returns: ["NUMBER"],
        compute: function (...values) {
            return variance(values, false, false);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // VAR.S
    // -----------------------------------------------------------------------------
    const VAR_S = {
        description: _lt("Variance."),
        args: args(`
      value1 (number, range<number>) ${_lt("The first value or range of the sample.")}
      value2 (number, range<number>, repeating) ${_lt("Additional values or ranges to include in the sample.")}
    `),
        returns: ["NUMBER"],
        compute: function (...values) {
            return variance(values, true, false);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // VARA
    // -----------------------------------------------------------------------------
    const VARA = {
        description: _lt("Variance of sample (text as 0)."),
        args: args(`
    value1 (number, range<number>) ${_lt("The first value or range of the sample.")}
    value2 (number, range<number>, repeating) ${_lt("Additional values or ranges to include in the sample.")}
  `),
        returns: ["NUMBER"],
        compute: function (...values) {
            return variance(values, true, true);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // VARP
    // -----------------------------------------------------------------------------
    const VARP = {
        description: _lt("Variance of entire population."),
        args: args(`
    value1 (number, range<number>) ${_lt("The first value or range of the population.")}
    value2 (number, range<number>, repeating) ${_lt("Additional values or ranges to include in the population.")}
  `),
        returns: ["NUMBER"],
        compute: function (...values) {
            return variance(values, false, false);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // VARPA
    // -----------------------------------------------------------------------------
    const VARPA = {
        description: _lt("Variance of entire population (text as 0)."),
        args: args(`
    value1 (number, range<number>) ${_lt("The first value or range of the population.")}
    value2 (number, range<number>, repeating) ${_lt("Additional values or ranges to include in the population.")}
  `),
        returns: ["NUMBER"],
        compute: function (...values) {
            return variance(values, false, true);
        },
        isExported: true,
    };

    var statistical = /*#__PURE__*/Object.freeze({
        __proto__: null,
        AVEDEV: AVEDEV,
        AVERAGE: AVERAGE,
        AVERAGE_WEIGHTED: AVERAGE_WEIGHTED,
        AVERAGEA: AVERAGEA,
        AVERAGEIF: AVERAGEIF,
        AVERAGEIFS: AVERAGEIFS,
        COUNT: COUNT,
        COUNTA: COUNTA,
        COVAR: COVAR,
        COVARIANCE_P: COVARIANCE_P,
        COVARIANCE_S: COVARIANCE_S,
        LARGE: LARGE,
        MAX: MAX,
        MAXA: MAXA,
        MAXIFS: MAXIFS,
        MEDIAN: MEDIAN,
        MIN: MIN,
        MINA: MINA,
        MINIFS: MINIFS,
        PERCENTILE: PERCENTILE,
        PERCENTILE_EXC: PERCENTILE_EXC,
        PERCENTILE_INC: PERCENTILE_INC,
        QUARTILE: QUARTILE,
        QUARTILE_EXC: QUARTILE_EXC,
        QUARTILE_INC: QUARTILE_INC,
        SMALL: SMALL,
        STDEV: STDEV,
        STDEV_P: STDEV_P,
        STDEV_S: STDEV_S,
        STDEVA: STDEVA,
        STDEVP: STDEVP,
        STDEVPA: STDEVPA,
        VAR: VAR,
        VAR_P: VAR_P,
        VAR_S: VAR_S,
        VARA: VARA,
        VARP: VARP,
        VARPA: VARPA
    });

    function getMatchingCells(database, field, criteria) {
        // Example
        var _a;
        // # 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(_lt("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(_lt("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 = (_a = indexColNameDB.get(colName)) !== null && _a !== void 0 ? _a : -1;
            if (index === -1) {
                throw new Error(_lt("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(_lt("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 !== undefined) {
                    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);
                    }, 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 = args(`
  database (range) ${_lt("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.")}
  field (any) ${_lt("Indicates which column in database contains the values to be extracted and operated on.")}
  criteria (range) ${_lt("An array or range containing zero or more criteria to filter the database values by before operating.")}
`);
    // -----------------------------------------------------------------------------
    // DAVERAGE
    // -----------------------------------------------------------------------------
    const DAVERAGE = {
        description: _lt("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);
            return AVERAGE.compute([cells]);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DCOUNT
    // -----------------------------------------------------------------------------
    const DCOUNT = {
        description: _lt("Counts values from a table-like range."),
        args: databaseArgs,
        returns: ["NUMBER"],
        compute: function (database, field, criteria) {
            const cells = getMatchingCells(database, field, criteria);
            return COUNT.compute([cells]);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DCOUNTA
    // -----------------------------------------------------------------------------
    const DCOUNTA = {
        description: _lt("Counts values and text from a table-like range."),
        args: databaseArgs,
        returns: ["NUMBER"],
        compute: function (database, field, criteria) {
            const cells = getMatchingCells(database, field, criteria);
            return COUNTA.compute([cells]);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DGET
    // -----------------------------------------------------------------------------
    const DGET = {
        description: _lt("Single value from a table-like range."),
        args: databaseArgs,
        returns: ["NUMBER"],
        compute: function (database, field, criteria) {
            const cells = getMatchingCells(database, field, criteria);
            assert(() => cells.length === 1, _lt("More than one match found in DGET evaluation."));
            return cells[0];
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DMAX
    // -----------------------------------------------------------------------------
    const DMAX = {
        description: _lt("Maximum of values from a table-like range."),
        args: databaseArgs,
        returns: ["NUMBER"],
        compute: function (database, field, criteria) {
            const cells = getMatchingCells(database, field, criteria);
            return MAX.compute([cells]);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DMIN
    // -----------------------------------------------------------------------------
    const DMIN = {
        description: _lt("Minimum of values from a table-like range."),
        args: databaseArgs,
        returns: ["NUMBER"],
        compute: function (database, field, criteria) {
            const cells = getMatchingCells(database, field, criteria);
            return MIN.compute([cells]);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DPRODUCT
    // -----------------------------------------------------------------------------
    const DPRODUCT = {
        description: _lt("Product of values from a table-like range."),
        args: databaseArgs,
        returns: ["NUMBER"],
        compute: function (database, field, criteria) {
            const cells = getMatchingCells(database, field, criteria);
            return PRODUCT.compute([cells]);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DSTDEV
    // -----------------------------------------------------------------------------
    const DSTDEV = {
        description: _lt("Standard deviation of population sample from table."),
        args: databaseArgs,
        returns: ["NUMBER"],
        compute: function (database, field, criteria) {
            const cells = getMatchingCells(database, field, criteria);
            return STDEV.compute([cells]);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DSTDEVP
    // -----------------------------------------------------------------------------
    const DSTDEVP = {
        description: _lt("Standard deviation of entire population from table."),
        args: databaseArgs,
        returns: ["NUMBER"],
        compute: function (database, field, criteria) {
            const cells = getMatchingCells(database, field, criteria);
            return STDEVP.compute([cells]);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DSUM
    // -----------------------------------------------------------------------------
    const DSUM = {
        description: _lt("Sum of values from a table-like range."),
        args: databaseArgs,
        returns: ["NUMBER"],
        compute: function (database, field, criteria) {
            const cells = getMatchingCells(database, field, criteria);
            return SUM.compute([cells]);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DVAR
    // -----------------------------------------------------------------------------
    const DVAR = {
        description: _lt("Variance of population sample from table-like range."),
        args: databaseArgs,
        returns: ["NUMBER"],
        compute: function (database, field, criteria) {
            const cells = getMatchingCells(database, field, criteria);
            return VAR.compute([cells]);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DVARP
    // -----------------------------------------------------------------------------
    const DVARP = {
        description: _lt("Variance of a population from a table-like range."),
        args: databaseArgs,
        returns: ["NUMBER"],
        compute: function (database, field, criteria) {
            const cells = getMatchingCells(database, field, criteria);
            return VARP.compute([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;
    // -----------------------------------------------------------------------------
    // DATE
    // -----------------------------------------------------------------------------
    const DATE = {
        description: _lt("Converts year/month/day into a date."),
        args: args(`
    year (number) ${_lt("The year component of the date.")}
    month (number) ${_lt("The month component of the date.")}
    day (number) ${_lt("The day component of the date.")}
    `),
        returns: ["DATE"],
        computeFormat: () => "m/d/yyyy",
        compute: function (year, month, day) {
            let _year = Math.trunc(toNumber(year));
            const _month = Math.trunc(toNumber(month));
            const _day = Math.trunc(toNumber(day));
            // For years less than 0 or greater than 10000, return #ERROR.
            assert(() => 0 <= _year && _year <= 9999, _lt("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, _lt(`The function [[FUNCTION_NAME]] result must be greater than or equal 01/01/1900.`));
            return result;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DATEVALUE
    // -----------------------------------------------------------------------------
    const DATEVALUE = {
        description: _lt("Converts a date string to a date value."),
        args: args(`
      date_string (string) ${_lt("The string representing the date.")}
    `),
        returns: ["NUMBER"],
        compute: function (dateString) {
            const _dateString = toString(dateString);
            const internalDate = parseDateTime(_dateString);
            assert(() => internalDate !== null, _lt("The date_string (%s) cannot be parsed to date/time.", _dateString.toString()));
            return Math.trunc(internalDate.value);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DAY
    // -----------------------------------------------------------------------------
    const DAY = {
        description: _lt("Day of the month that a specific date falls on."),
        args: args(`
      date (string) ${_lt("The date from which to extract the day.")}
    `),
        returns: ["NUMBER"],
        compute: function (date) {
            return toJsDate(date).getDate();
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DAYS
    // -----------------------------------------------------------------------------
    const DAYS = {
        description: _lt("Number of days between two dates."),
        args: args(`
      end_date (date) ${_lt("The end of the date range.")}
      start_date (date) ${_lt("The start of the date range.")}
    `),
        returns: ["NUMBER"],
        compute: function (endDate, startDate) {
            const _endDate = toJsDate(endDate);
            const _startDate = toJsDate(startDate);
            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: _lt("Number of days between two dates on a 360-day year (months of 30 days)."),
        args: args(`
      start_date (date) ${_lt("The start date to consider in the calculation.")}
      end_date (date) ${_lt("The end date to consider in the calculation.")}
      method (number, default=${DEFAULT_DAY_COUNT_METHOD}) ${_lt("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);
            const _endDate = toNumber(endDate);
            const dayCountConvention = toBoolean(method) ? 4 : 0;
            const yearFrac = YEARFRAC.compute(startDate, endDate, dayCountConvention);
            return Math.sign(_endDate - _startDate) * Math.round(yearFrac * 360);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // EDATE
    // -----------------------------------------------------------------------------
    const EDATE = {
        description: _lt("Date a number of months before/after another date."),
        args: args(`
    start_date (date) ${_lt("The date from which to calculate the result.")}
    months (number) ${_lt("The number of months before (negative) or after (positive) 'start_date' to calculate.")}
    `),
        returns: ["DATE"],
        computeFormat: () => "m/d/yyyy",
        compute: function (startDate, months) {
            const _startDate = toJsDate(startDate);
            const _months = Math.trunc(toNumber(months));
            const jsDate = addMonthsToDate(_startDate, _months, false);
            return jsDateToRoundNumber(jsDate);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // EOMONTH
    // -----------------------------------------------------------------------------
    const EOMONTH = {
        description: _lt("Last day of a month before or after a date."),
        args: args(`
    start_date (date) ${_lt("The date from which to calculate the result.")}
    months (number) ${_lt("The number of months before (negative) or after (positive) 'start_date' to consider.")}
    `),
        returns: ["DATE"],
        computeFormat: () => "m/d/yyyy",
        compute: function (startDate, months) {
            const _startDate = toJsDate(startDate);
            const _months = Math.trunc(toNumber(months));
            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: _lt("Hour component of a specific time."),
        args: args(`
    time (date) ${_lt("The time from which to calculate the hour component.")}
    `),
        returns: ["NUMBER"],
        compute: function (date) {
            return toJsDate(date).getHours();
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ISOWEEKNUM
    // -----------------------------------------------------------------------------
    const ISOWEEKNUM = {
        description: _lt("ISO week number of the year."),
        args: args(`
    date (date) ${_lt("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);
            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: _lt("Minute component of a specific time."),
        args: args(`
      time (date) ${_lt("The time from which to calculate the minute component.")}
    `),
        returns: ["NUMBER"],
        compute: function (date) {
            return toJsDate(date).getMinutes();
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // MONTH
    // -----------------------------------------------------------------------------
    const MONTH = {
        description: _lt("Month of the year a specific date falls in"),
        args: args(`
      date (date) ${_lt("The date from which to extract the month.")}
    `),
        returns: ["NUMBER"],
        compute: function (date) {
            return toJsDate(date).getMonth() + 1;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // NETWORKDAYS
    // -----------------------------------------------------------------------------
    const NETWORKDAYS = {
        description: _lt("Net working days between two provided days."),
        args: args(`
      start_date (date) ${_lt("The start date of the period from which to calculate the number of net working days.")}
      end_date (date) ${_lt("The end date of the period from which to calculate the number of net working days.")}
      holidays (date, range<date>, optional) ${_lt("A range or array constant containing the date serial numbers to consider holidays.")}
    `),
        returns: ["NUMBER"],
        compute: function (startDate, endDate, holidays) {
            return NETWORKDAYS_INTL.compute(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;
            }, _lt('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), _lt("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(_lt("The weekend must be a number or a string."));
    }
    const NETWORKDAYS_INTL = {
        description: _lt("Net working days between two dates (specifying weekends)."),
        args: args(`
      start_date (date) ${_lt("The start date of the period from which to calculate the number of net working days.")}
      end_date (date) ${_lt("The end date of the period from which to calculate the number of net working days.")}
      weekend (any, default=${DEFAULT_WEEKEND}) ${_lt("A number or string representing which days of the week are considered weekends.")}
      holidays (date, range<date>, optional) ${_lt("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);
            const _endDate = toJsDate(endDate);
            const daysWeekend = weekendToDayNumber(weekend);
            let timesHoliday = new Set();
            if (holidays !== undefined) {
                visitAny([holidays], (h) => {
                    const holiday = toJsDate(h);
                    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: _lt("Current date and time as a date value."),
        args: [],
        returns: ["DATE"],
        computeFormat: () => "m/d/yyyy hh:mm:ss",
        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: _lt("Minute component of a specific time."),
        args: args(`
      time (date) ${_lt("The time from which to calculate the second component.")}
    `),
        returns: ["NUMBER"],
        compute: function (date) {
            return toJsDate(date).getSeconds();
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // TIME
    // -----------------------------------------------------------------------------
    const TIME = {
        description: _lt("Converts hour/minute/second into a time."),
        args: args(`
    hour (number) ${_lt("The hour component of the time.")}
    minute (number) ${_lt("The minute component of the time.")}
    second (number) ${_lt("The second component of the time.")}
    `),
        returns: ["DATE"],
        computeFormat: () => "hh:mm:ss a",
        compute: function (hour, minute, second) {
            let _hour = Math.trunc(toNumber(hour));
            let _minute = Math.trunc(toNumber(minute));
            let _second = Math.trunc(toNumber(second));
            _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, _lt(`The function [[FUNCTION_NAME]] result cannot be negative`));
            return _hour / 24 + _minute / (24 * 60) + _second / (24 * 60 * 60);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // TIMEVALUE
    // -----------------------------------------------------------------------------
    const TIMEVALUE = {
        description: _lt("Converts a time string into its serial number representation."),
        args: args(`
      time_string (string) ${_lt("The string that holds the time representation.")}
    `),
        returns: ["NUMBER"],
        compute: function (timeString) {
            const _timeString = toString(timeString);
            const internalDate = parseDateTime(_timeString);
            assert(() => internalDate !== null, _lt("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: _lt("Current date as a date value."),
        args: [],
        returns: ["DATE"],
        computeFormat: () => "m/d/yyyy",
        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: _lt("Day of the week of the date provided (as number)."),
        args: args(`
    date (date) ${_lt("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.")}
    type (number, default=${DEFAULT_TYPE}) ${_lt("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);
            const _type = Math.round(toNumber(type));
            const m = _date.getDay();
            assert(() => [1, 2, 3].includes(_type), _lt("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: _lt("Week number of the year."),
        args: args(`
    date (date) ${_lt("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.")}
    type (number, default=${DEFAULT_TYPE}) ${_lt("A number representing the day that a week starts on. Sunday = 1.")}
    `),
        returns: ["NUMBER"],
        compute: function (date, type = DEFAULT_TYPE) {
            const _date = toJsDate(date);
            const _type = Math.round(toNumber(type));
            assert(() => _type === 1 || _type === 2 || (11 <= _type && _type <= 17) || _type === 21, _lt("The type (%s) is out of range.", _type.toString()));
            if (_type === 21) {
                return ISOWEEKNUM.compute(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: _lt("Date after a number of workdays."),
        args: args(`
      start_date (date) ${_lt("The date from which to begin counting.")}
      num_days (number) ${_lt("The number of working days to advance from start_date. If negative, counts backwards.")}
      holidays (date, range<date>, optional) ${_lt("A range or array constant containing the dates to consider holidays.")}
      `),
        returns: ["NUMBER"],
        computeFormat: () => "m/d/yyyy",
        compute: function (startDate, numDays, holidays = undefined) {
            return WORKDAY_INTL.compute(startDate, numDays, 1, holidays);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // WORKDAY.INTL
    // -----------------------------------------------------------------------------
    const WORKDAY_INTL = {
        description: _lt("Date after a number of workdays (specifying weekends)."),
        args: args(`
      start_date (date) ${_lt("The date from which to begin counting.")}
      num_days (number) ${_lt("The number of working days to advance from start_date. If negative, counts backwards.")}
      weekend (any, default=${DEFAULT_WEEKEND}) ${_lt("A number or string representing which days of the week are considered weekends.")}
      holidays (date, range<date>, optional) ${_lt("A range or array constant containing the dates to consider holidays.")}
    `),
        returns: ["DATE"],
        computeFormat: () => "m/d/yyyy",
        compute: function (startDate, numDays, weekend = DEFAULT_WEEKEND, holidays) {
            let _startDate = toJsDate(startDate);
            let _numDays = Math.trunc(toNumber(numDays));
            if (typeof weekend === "string") {
                assert(() => weekend !== "1111111", _lt("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);
                    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: _lt("Year specified by a given date."),
        args: args(`
    date (date) ${_lt("The date from which to extract the year.")}
    `),
        returns: ["NUMBER"],
        compute: function (date) {
            return toJsDate(date).getFullYear();
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // YEARFRAC
    // -----------------------------------------------------------------------------
    const DEFAULT_DAY_COUNT_CONVENTION$1 = 0;
    const YEARFRAC = {
        description: _lt("Exact number of years between two dates."),
        args: args(`
    start_date (date) ${_lt("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.")}
    end_date (date) ${_lt("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.")}
    day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION$1}) ${_lt("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));
            let _endDate = Math.trunc(toNumber(endDate));
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention));
            assert(() => _startDate >= 0, _lt("The start_date (%s) must be positive or null.", _startDate.toString()));
            assert(() => _endDate >= 0, _lt("The end_date (%s) must be positive or null.", _endDate.toString()));
            assert(() => 0 <= _dayCountConvention && _dayCountConvention <= 4, _lt("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: _lt("First day of the month preceding a date."),
        args: args(`
    date (date) ${_lt("The date from which to calculate the result.")}
    `),
        returns: ["DATE"],
        computeFormat: () => "m/d/yyyy",
        compute: function (date) {
            const _startDate = toJsDate(date);
            const yStart = _startDate.getFullYear();
            const mStart = _startDate.getMonth();
            const jsDate = new DateTime(yStart, mStart, 1);
            return jsDateToRoundNumber(jsDate);
        },
    };
    // -----------------------------------------------------------------------------
    // MONTH.END
    // -----------------------------------------------------------------------------
    const MONTH_END = {
        description: _lt("Last day of the month following a date."),
        args: args(`
    date (date) ${_lt("The date from which to calculate the result.")}
    `),
        returns: ["DATE"],
        computeFormat: () => "m/d/yyyy",
        compute: function (date) {
            return EOMONTH.compute(date, 0);
        },
    };
    // -----------------------------------------------------------------------------
    // QUARTER
    // -----------------------------------------------------------------------------
    const QUARTER = {
        description: _lt("Quarter of the year a specific date falls in"),
        args: args(`
    date (date) ${_lt("The date from which to extract the quarter.")}
    `),
        returns: ["NUMBER"],
        compute: function (date) {
            return Math.ceil((toJsDate(date).getMonth() + 1) / 3);
        },
    };
    // -----------------------------------------------------------------------------
    // QUARTER.START
    // -----------------------------------------------------------------------------
    const QUARTER_START = {
        description: _lt("First day of the quarter of the year a specific date falls in."),
        args: args(`
    date (date) ${_lt("The date from which to calculate the start of quarter.")}
    `),
        returns: ["DATE"],
        computeFormat: () => "m/d/yyyy",
        compute: function (date) {
            const quarter = QUARTER.compute(date);
            const year = YEAR.compute(date);
            const jsDate = new DateTime(year, (quarter - 1) * 3, 1);
            return jsDateToRoundNumber(jsDate);
        },
    };
    // -----------------------------------------------------------------------------
    // QUARTER.END
    // -----------------------------------------------------------------------------
    const QUARTER_END = {
        description: _lt("Last day of the quarter of the year a specific date falls in."),
        args: args(`
    date (date) ${_lt("The date from which to calculate the end of quarter.")}
    `),
        returns: ["DATE"],
        computeFormat: () => "m/d/yyyy",
        compute: function (date) {
            const quarter = QUARTER.compute(date);
            const year = YEAR.compute(date);
            const jsDate = new DateTime(year, quarter * 3, 0);
            return jsDateToRoundNumber(jsDate);
        },
    };
    // -----------------------------------------------------------------------------
    // YEAR.START
    // -----------------------------------------------------------------------------
    const YEAR_START = {
        description: _lt("First day of the year a specific date falls in."),
        args: args(`
    date (date) ${_lt("The date from which to calculate the start of the year.")}
    `),
        returns: ["DATE"],
        computeFormat: () => "m/d/yyyy",
        compute: function (date) {
            const year = YEAR.compute(date);
            const jsDate = new DateTime(year, 0, 1);
            return jsDateToRoundNumber(jsDate);
        },
    };
    // -----------------------------------------------------------------------------
    // YEAR.END
    // -----------------------------------------------------------------------------
    const YEAR_END = {
        description: _lt("Last day of the year a specific date falls in."),
        args: args(`
    date (date) ${_lt("The date from which to calculate the end of the year.")}
    `),
        returns: ["DATE"],
        computeFormat: () => "m/d/yyyy",
        compute: function (date) {
            const year = YEAR.compute(date);
            const jsDate = new DateTime(year + 1, 0, 0);
            return jsDateToRoundNumber(jsDate);
        },
    };

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

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

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

    /** Assert maturity date > settlement date */
    function assertMaturityAndSettlementDatesAreValid(settlement, maturity) {
        assert(() => settlement < maturity, _lt("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, _lt("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), _lt("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, _lt("The day_count_convention (%s) must be between 0 and 4 inclusive.", dayCountConvention.toString()));
    }
    function assertRedemptionStrictlyPositive(redemption) {
        assert(() => redemption > 0, _lt("The redemption (%s) must be strictly positive.", redemption.toString()));
    }
    function assertPriceStrictlyPositive(price) {
        assert(() => price > 0, _lt("The price (%s) must be strictly positive.", price.toString()));
    }
    function assertNumberOfPeriodsStrictlyPositive(nPeriods) {
        assert(() => nPeriods > 0, _lt("The number_of_periods (%s) must be greater than 0.", nPeriods.toString()));
    }
    function assertRateStrictlyPositive(rate) {
        assert(() => rate > 0, _lt("The rate (%s) must be strictly positive.", rate.toString()));
    }
    function assertLifeStrictlyPositive(life) {
        assert(() => life > 0, _lt("The life (%s) must be strictly positive.", life.toString()));
    }
    function assertCostStrictlyPositive(cost) {
        assert(() => cost > 0, _lt("The cost (%s) must be strictly positive.", cost.toString()));
    }
    function assertCostPositiveOrZero(cost) {
        assert(() => cost >= 0, _lt("The cost (%s) must be positive or null.", cost.toString()));
    }
    function assertPeriodStrictlyPositive(period) {
        assert(() => period > 0, _lt("The period (%s) must be strictly positive.", period.toString()));
    }
    function assertPeriodPositiveOrZero(period) {
        assert(() => period >= 0, _lt("The period (%s) must be positive or null.", period.toString()));
    }
    function assertSalvagePositiveOrZero(salvage) {
        assert(() => salvage >= 0, _lt("The salvage (%s) must be positive or null.", salvage.toString()));
    }
    function assertSalvageSmallerOrEqualThanCost(salvage, cost) {
        assert(() => salvage <= cost, _lt("The salvage (%s) must be smaller or equal than the cost (%s).", salvage.toString(), cost.toString()));
    }
    function assertPresentValueStrictlyPositive(pv) {
        assert(() => pv > 0, _lt("The present value (%s) must be strictly positive.", pv.toString()));
    }
    function assertPeriodSmallerOrEqualToLife(period, life) {
        assert(() => period <= life, _lt("The period (%s) must be less than or equal life (%.", period.toString(), life.toString()));
    }
    function assertInvestmentStrictlyPositive(investment) {
        assert(() => investment > 0, _lt("The investment (%s) must be strictly positive.", investment.toString()));
    }
    function assertDiscountStrictlyPositive(discount) {
        assert(() => discount > 0, _lt("The discount (%s) must be strictly positive.", discount.toString()));
    }
    function assertDiscountStrictlySmallerThanOne(discount) {
        assert(() => discount < 1, _lt("The discount (%s) must be smaller than 1.", discount.toString()));
    }
    function assertDeprecationFactorStrictlyPositive(factor) {
        assert(() => factor > 0, _lt("The depreciation factor (%s) must be strictly positive.", factor.toString()));
    }
    function assertSettlementLessThanOneYearBeforeMaturity(settlement, maturity) {
        const startDate = toJsDate(settlement);
        const endDate = toJsDate(maturity);
        const startDatePlusOneYear = toJsDate(settlement);
        startDatePlusOneYear.setFullYear(startDate.getFullYear() + 1);
        assert(() => endDate.getTime() <= startDatePlusOneYear.getTime(), _lt("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, _lt("The first_period (%s) must be strictly positive.", firstPeriod.toString()));
        assert(() => lastPeriod > 0, _lt("The last_period (%s) must be strictly positive.", lastPeriod.toString()));
        assert(() => firstPeriod <= lastPeriod, _lt("The first_period (%s) must be smaller or equal to the last_period (%s).", firstPeriod.toString(), lastPeriod.toString()));
        assert(() => lastPeriod <= numberOfPeriods, _lt("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, _lt("The start_period (%s) must be greater or equal than 0.", startPeriod.toString()));
        assert(() => endPeriod >= 0, _lt("The end_period (%s) must be greater or equal than 0.", endPeriod.toString()));
        assert(() => startPeriod <= endPeriod, _lt("The start_period (%s) must be smaller or equal to the end_period (%s).", startPeriod.toString(), endPeriod.toString()));
        assert(() => endPeriod <= life, _lt("The end_period (%s) must be smaller or equal to the life (%s).", startPeriod.toString(), life.toString()));
    }
    function assertRateGuessStrictlyGreaterThanMinusOne(guess) {
        assert(() => guess > -1, _lt("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, _lt("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), _lt("There must be both positive and negative values in cashflow_amounts."));
    }
    function assertEveryDateGreaterThanFirstDateOfCashFlowDates(dates) {
        assert(() => dates.every((date) => date >= dates[0]), _lt("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 = args(`
settlement (date) ${_lt("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")}
maturity (date) ${_lt("The maturity or end date of the security, when it can be redeemed at face, or par value.")}
frequency (number) ${_lt("The number of interest or coupon payments per year (1, 2, or 4).")}
day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("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, _lt(`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, _lt(`Function [[FUNCTION_NAME]] didn't find any result.`));
            count++;
        } while (!yEqual0);
        return x;
    }
    // -----------------------------------------------------------------------------
    // ACCRINTM
    // -----------------------------------------------------------------------------
    const ACCRINTM = {
        description: _lt("Accrued interest of security paying at maturity."),
        args: args(`
        issue (date) ${_lt("The date the security was initially issued.")}
        maturity (date) ${_lt("The maturity date of the security.")}
        rate (number) ${_lt("The annualized rate of interest.")}
        redemption (number) ${_lt("The redemption amount per 100 face value, or par.")}
        day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("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));
            const end = Math.trunc(toNumber(maturity));
            const _redemption = toNumber(redemption);
            const _rate = toNumber(rate);
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention));
            assertSettlementAndIssueDatesAreValid(end, start);
            assertDayCountConventionIsValid(_dayCountConvention);
            assertRedemptionStrictlyPositive(_redemption);
            assertRateStrictlyPositive(_rate);
            const yearFrac = YEARFRAC.compute(start, end, dayCountConvention);
            return _redemption * _rate * yearFrac;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // AMORLINC
    // -----------------------------------------------------------------------------
    const AMORLINC = {
        description: _lt("Depreciation for an accounting period."),
        args: args(`
        cost (number) ${_lt("The initial cost of the asset.")}
        purchase_date (date) ${_lt("The date the asset was purchased.")}
        first_period_end (date) ${_lt("The date the first period ended.")}
        salvage (number) ${_lt("The value of the asset at the end of depreciation.")}
        period (number) ${_lt("The single period within life for which to calculate depreciation.")}
        rate (number) ${_lt("The deprecation rate.")}
        day_count_convention  (number, optional) ${_lt("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);
            const _purchaseDate = Math.trunc(toNumber(purchaseDate));
            const _firstPeriodEnd = Math.trunc(toNumber(firstPeriodEnd));
            const _salvage = toNumber(salvage);
            const _period = toNumber(period);
            const _rate = toNumber(rate);
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention));
            assertCostStrictlyPositive(_cost);
            assertSalvagePositiveOrZero(_salvage);
            assertSalvageSmallerOrEqualThanCost(_salvage, _cost);
            assertPeriodPositiveOrZero(_period);
            assertRateStrictlyPositive(_rate);
            assertDayCountConventionIsValid(_dayCountConvention);
            assert(() => _purchaseDate <= _firstPeriodEnd, _lt("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(_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: _lt("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));
            const end = Math.trunc(toNumber(maturity));
            const _frequency = Math.trunc(toNumber(frequency));
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention));
            assertMaturityAndSettlementDatesAreValid(start, end);
            assertCouponFrequencyIsValid(_frequency);
            assertDayCountConventionIsValid(_dayCountConvention);
            // https://wiki.documentfoundation.org/Documentation/Calc_Functions/COUPDAYS
            if (_dayCountConvention === 1) {
                const before = COUPPCD.compute(settlement, maturity, frequency, dayCountConvention);
                const after = COUPNCD.compute(settlement, maturity, frequency, dayCountConvention);
                return after - before;
            }
            const daysInYear = _dayCountConvention === 3 ? 365 : 360;
            return daysInYear / _frequency;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COUPDAYBS
    // -----------------------------------------------------------------------------
    const COUPDAYBS = {
        description: _lt("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));
            const end = Math.trunc(toNumber(maturity));
            const _frequency = Math.trunc(toNumber(frequency));
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention));
            assertMaturityAndSettlementDatesAreValid(start, end);
            assertCouponFrequencyIsValid(_frequency);
            assertDayCountConventionIsValid(_dayCountConvention);
            const couponBeforeStart = COUPPCD.compute(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);
            const dateCouponBeforeStart = toJsDate(couponBeforeStart);
            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: _lt("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));
            const end = Math.trunc(toNumber(maturity));
            const _frequency = Math.trunc(toNumber(frequency));
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention));
            assertMaturityAndSettlementDatesAreValid(start, end);
            assertCouponFrequencyIsValid(_frequency);
            assertDayCountConventionIsValid(_dayCountConvention);
            const couponAfterStart = COUPNCD.compute(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(settlement, maturity, frequency, _dayCountConvention);
            const coupDays = COUPDAYS.compute(settlement, maturity, frequency, _dayCountConvention);
            return coupDays - coupDayBs;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COUPNCD
    // -----------------------------------------------------------------------------
    const COUPNCD = {
        description: _lt("Next coupon date after the settlement date."),
        args: COUPON_FUNCTION_ARGS,
        returns: ["NUMBER"],
        computeFormat: () => "m/d/yyyy",
        compute: function (settlement, maturity, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) {
            dayCountConvention = dayCountConvention || 0;
            const start = Math.trunc(toNumber(settlement));
            const end = Math.trunc(toNumber(maturity));
            const _frequency = Math.trunc(toNumber(frequency));
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention));
            assertMaturityAndSettlementDatesAreValid(start, end);
            assertCouponFrequencyIsValid(_frequency);
            assertDayCountConventionIsValid(_dayCountConvention);
            const monthsPerPeriod = 12 / _frequency;
            const coupNum = COUPNUM.compute(settlement, maturity, frequency, dayCountConvention);
            const date = addMonthsToDate(toJsDate(end), -(coupNum - 1) * monthsPerPeriod, true);
            return jsDateToRoundNumber(date);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COUPNUM
    // -----------------------------------------------------------------------------
    const COUPNUM = {
        description: _lt("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));
            const end = Math.trunc(toNumber(maturity));
            const _frequency = Math.trunc(toNumber(frequency));
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention));
            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), -monthsPerPeriod, false));
                num++;
            }
            return num - 1;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COUPPCD
    // -----------------------------------------------------------------------------
    const COUPPCD = {
        description: _lt("Last coupon date prior to or on the settlement date."),
        args: COUPON_FUNCTION_ARGS,
        returns: ["NUMBER"],
        computeFormat: () => "m/d/yyyy",
        compute: function (settlement, maturity, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) {
            dayCountConvention = dayCountConvention || 0;
            const start = Math.trunc(toNumber(settlement));
            const end = Math.trunc(toNumber(maturity));
            const _frequency = Math.trunc(toNumber(frequency));
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention));
            assertMaturityAndSettlementDatesAreValid(start, end);
            assertCouponFrequencyIsValid(_frequency);
            assertDayCountConventionIsValid(_dayCountConvention);
            const monthsPerPeriod = 12 / _frequency;
            const coupNum = COUPNUM.compute(settlement, maturity, frequency, dayCountConvention);
            const date = addMonthsToDate(toJsDate(end), -coupNum * monthsPerPeriod, true);
            return jsDateToRoundNumber(date);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // CUMIPMT
    // -----------------------------------------------------------------------------
    const CUMIPMT = {
        description: _lt("Cumulative interest paid over a set of periods."),
        args: args(`
  rate (number) ${_lt("The interest rate.")}
  number_of_periods (number) ${_lt("The number of payments to be made.")}
  present_value (number) ${_lt("The current value of the annuity.")}
  first_period (number) ${_lt("The number of the payment period to begin the cumulative calculation.")}
  last_period (number) ${_lt("The number of the payment period to end the cumulative calculation.")}
  end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING}) ${_lt("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);
            const last = toNumber(lastPeriod);
            const _rate = toNumber(rate);
            const pv = toNumber(presentValue);
            const nOfPeriods = toNumber(numberOfPeriods);
            assertFirstAndLastPeriodsAreValid(first, last, nOfPeriods);
            assertRateStrictlyPositive(_rate);
            assertPresentValueStrictlyPositive(pv);
            let cumSum = 0;
            for (let i = first; i <= last; i++) {
                const impt = IPMT.compute(rate, i, nOfPeriods, presentValue, 0, endOrBeginning);
                cumSum += impt;
            }
            return cumSum;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // CUMPRINC
    // -----------------------------------------------------------------------------
    const CUMPRINC = {
        description: _lt("Cumulative principal paid over a set of periods."),
        args: args(`
  rate (number) ${_lt("The interest rate.")}
  number_of_periods (number) ${_lt("The number of payments to be made.")}
  present_value (number) ${_lt("The current value of the annuity.")}
  first_period (number) ${_lt("The number of the payment period to begin the cumulative calculation.")}
  last_period (number) ${_lt("The number of the payment period to end the cumulative calculation.")}
  end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING}) ${_lt("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);
            const last = toNumber(lastPeriod);
            const _rate = toNumber(rate);
            const pv = toNumber(presentValue);
            const nOfPeriods = toNumber(numberOfPeriods);
            assertFirstAndLastPeriodsAreValid(first, last, nOfPeriods);
            assertRateStrictlyPositive(_rate);
            assertPresentValueStrictlyPositive(pv);
            let cumSum = 0;
            for (let i = first; i <= last; i++) {
                const ppmt = PPMT.compute(rate, i, nOfPeriods, presentValue, 0, endOrBeginning);
                cumSum += ppmt;
            }
            return cumSum;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DB
    // -----------------------------------------------------------------------------
    const DB = {
        description: _lt("Depreciation via declining balance method."),
        args: args(`
        cost (number) ${_lt("The initial cost of the asset.")}
        salvage (number) ${_lt("The value of the asset at the end of depreciation.")}
        life (number) ${_lt("The number of periods over which the asset is depreciated.")}
        period (number) ${_lt("The single period within life for which to calculate depreciation.")}
        month (number, optional) ${_lt("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);
            const _salvage = toNumber(salvage);
            const _life = toNumber(life);
            const _period = Math.trunc(toNumber(period));
            const _month = args.length ? Math.trunc(toNumber(args[0])) : 12;
            const lifeLimit = _life + (_month === 12 ? 0 : 1);
            assertCostPositiveOrZero(_cost);
            assertSalvagePositiveOrZero(_salvage);
            assertPeriodStrictlyPositive(_period);
            assertLifeStrictlyPositive(_life);
            assert(() => 1 <= _month && _month <= 12, _lt("The month (%s) must be between 1 and 12 inclusive.", _month.toString()));
            assert(() => _period <= lifeLimit, _lt("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: _lt("Depreciation via double-declining balance method."),
        args: args(`
        cost (number) ${_lt("The initial cost of the asset.")}
        salvage (number) ${_lt("The value of the asset at the end of depreciation.")}
        life (number) ${_lt("The number of periods over which the asset is depreciated.")}
        period (number) ${_lt("The single period within life for which to calculate depreciation.")}
        factor (number, default=${DEFAULT_DDB_DEPRECIATION_FACTOR}) ${_lt("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);
            const _salvage = toNumber(salvage);
            const _life = toNumber(life);
            const _period = toNumber(period);
            const _factor = toNumber(factor);
            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: _lt("Discount rate of a security based on price."),
        args: args(`
      settlement (date) ${_lt("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")}
      maturity (date) ${_lt("The maturity or end date of the security, when it can be redeemed at face, or par value.")}
      price (number) ${_lt("The price at which the security is bought per 100 face value.")}
      redemption (number) ${_lt("The redemption amount per 100 face value, or par.")}
      day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("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));
            const _maturity = Math.trunc(toNumber(maturity));
            const _price = toNumber(price);
            const _redemption = toNumber(redemption);
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention));
            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(_settlement, _maturity, _dayCountConvention);
            return (_redemption - _price) / _redemption / yearsFrac;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DOLLARDE
    // -----------------------------------------------------------------------------
    const DOLLARDE = {
        description: _lt("Convert a decimal fraction to decimal value."),
        args: args(`
      fractional_price (number) ${_lt("The price quotation given using fractional decimal conventions.")}
      unit (number) ${_lt("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);
            const _unit = Math.trunc(toNumber(unit));
            assert(() => _unit > 0, _lt("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: _lt("Convert a decimal value to decimal fraction."),
        args: args(`
  decimal_price (number) ${_lt("The price quotation given as a decimal value.")}
      unit (number) ${_lt("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);
            const _unit = Math.trunc(toNumber(unit));
            assert(() => _unit > 0, _lt("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: _lt("Number of periods for an investment to reach a value."),
        args: args(`
        settlement (date) ${_lt("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")}
        maturity (date) ${_lt("The maturity or end date of the security, when it can be redeemed at face, or par value.")}
        rate (number) ${_lt("The annualized rate of interest.")}
        yield (number) ${_lt("The expected annual yield of the security.")}
        frequency (number) ${_lt("The number of interest or coupon payments per year (1, 2, or 4).")}
        day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("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));
            const end = Math.trunc(toNumber(maturity));
            const _rate = toNumber(rate);
            const _yield = toNumber(securityYield);
            const _frequency = Math.trunc(toNumber(frequency));
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention));
            assertMaturityAndSettlementDatesAreValid(start, end);
            assertCouponFrequencyIsValid(_frequency);
            assertDayCountConventionIsValid(_dayCountConvention);
            assert(() => _rate >= 0, _lt("The rate (%s) must be positive or null.", _rate.toString()));
            assert(() => _yield >= 0, _lt("The yield (%s) must be positive or null.", _yield.toString()));
            const years = YEARFRAC.compute(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: _lt("Annual effective interest rate."),
        args: args(`
  nominal_rate (number) ${_lt("The nominal interest rate per year.")}
  periods_per_year (number) ${_lt("The number of compounding periods per year.")}
  `),
        returns: ["NUMBER"],
        compute: function (nominal_rate, periods_per_year) {
            const nominal = toNumber(nominal_rate);
            const periods = Math.trunc(toNumber(periods_per_year));
            assert(() => nominal > 0, _lt("The nominal rate (%s) must be strictly greater than 0.", nominal.toString()));
            assert(() => periods > 0, _lt("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: _lt("Future value of an annuity investment."),
        args: args(`
  rate (number) ${_lt("The interest rate.")}
  number_of_periods (number) ${_lt("The number of payments to be made.")}
  payment_amount (number) ${_lt("The amount per period to be paid.")}
  present_value (number, default=${DEFAULT_PRESENT_VALUE}) ${_lt("The current value of the annuity.")}
  end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING}) ${_lt("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);
            const n = toNumber(numberOfPeriods);
            const p = toNumber(paymentAmount);
            const pv = toNumber(presentValue);
            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: _lt("Future value of principal from series of rates."),
        args: args(`
  principal (number) ${_lt("The amount of initial capital or value to compound against.")}
  rate_schedule (number, range<number>) ${_lt("A series of interest rates to compound against the principal.")}
  `),
        returns: ["NUMBER"],
        compute: function (principalAmount, rateSchedule) {
            const principal = toNumber(principalAmount);
            return reduceAny([rateSchedule], (acc, rate) => acc * (1 + toNumber(rate)), principal);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // INTRATE
    // -----------------------------------------------------------------------------
    const INTRATE = {
        description: _lt("Calculates effective interest rate."),
        args: args(`
      settlement (date) ${_lt("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")}
      maturity (date) ${_lt("The maturity or end date of the security, when it can be redeemed at face, or par value.")}
      investment (number) ${_lt("The amount invested in the security.")}
      redemption (number) ${_lt("The amount to be received at maturity.")}
      day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("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));
            const _maturity = Math.trunc(toNumber(maturity));
            const _redemption = toNumber(redemption);
            const _investment = toNumber(investment);
            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(_settlement, _maturity, dayCountConvention);
            return (_redemption - _investment) / _investment / yearFrac;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // IPMT
    // -----------------------------------------------------------------------------
    const IPMT = {
        description: _lt("Payment on the principal of an investment."),
        args: args(`
  rate (number) ${_lt("The annualized rate of interest.")}
  period (number) ${_lt("The amortization period, in terms of number of periods.")}
  number_of_periods (number) ${_lt("The number of payments to be made.")}
  present_value (number) ${_lt("The current value of the annuity.")}
  future_value (number, default=${DEFAULT_FUTURE_VALUE}) ${_lt("The future value remaining after the final payment has been made.")}
  end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING}) ${_lt("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(rate, numberOfPeriods, presentValue, futureValue, endOrBeginning);
            const ppmt = PPMT.compute(rate, currentPeriod, numberOfPeriods, presentValue, futureValue, endOrBeginning);
            return payment - ppmt;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // IRR
    // -----------------------------------------------------------------------------
    const DEFAULT_RATE_GUESS = 0.1;
    const IRR = {
        description: _lt("Internal rate of return given periodic cashflows."),
        args: args(`
  cashflow_amounts (number, range<number>) ${_lt("An array or range containing the income or payments associated with the investment.")}
  rate_guess (number, default=${DEFAULT_RATE_GUESS}) ${_lt("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);
            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);
            });
            assert(() => positive && negative, _lt("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: _lt("Returns the interest paid at a particular period of an investment."),
        args: args(`
  rate (number) ${_lt("The interest rate.")}
  period (number) ${_lt("The period for which you want to view the interest payment.")}
  number_of_periods (number) ${_lt("The number of payments to be made.")}
  present_value (number) ${_lt("The current value of the annuity.")}
  `),
        returns: ["NUMBER"],
        compute: function (rate, currentPeriod, numberOfPeriods, presentValue) {
            const interestRate = toNumber(rate);
            const period = toNumber(currentPeriod);
            const nOfPeriods = toNumber(numberOfPeriods);
            const investment = toNumber(presentValue);
            assert(() => nOfPeriods !== 0, _lt("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: _lt("Modified Macaulay duration."),
        args: args(`
        settlement (date) ${_lt("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")}
        maturity (date) ${_lt("The maturity or end date of the security, when it can be redeemed at face, or par value.")}
        rate (number) ${_lt("The annualized rate of interest.")}
        yield (number) ${_lt("The expected annual yield of the security.")}
        frequency (number) ${_lt("The number of interest or coupon payments per year (1, 2, or 4).")}
        day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("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(settlement, maturity, rate, securityYield, frequency, dayCountConvention);
            const y = toNumber(securityYield);
            const k = Math.trunc(toNumber(frequency));
            return duration / (1 + y / k);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // MIRR
    // -----------------------------------------------------------------------------
    const MIRR = {
        description: _lt("Modified internal rate of return."),
        args: args(`
  cashflow_amounts (range<number>) ${_lt("A range containing the income or payments associated with the investment. The array should contain bot payments and incomes.")}
  financing_rate (number) ${_lt("The interest rate paid on funds invested.")}
  reinvestment_return_rate (number) ${_lt("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);
            const rRate = toNumber(reinvestmentRate);
            const cashFlow = transpose2dArray(cashflowAmount).flat().filter(isDefined$1).map(toNumber);
            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, _lt("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: _lt("Annual nominal interest rate."),
        args: args(`
  effective_rate (number) ${_lt("The effective interest rate per year.")}
  periods_per_year (number) ${_lt("The number of compounding periods per year.")}
  `),
        returns: ["NUMBER"],
        compute: function (effective_rate, periods_per_year) {
            const effective = toNumber(effective_rate);
            const periods = Math.trunc(toNumber(periods_per_year));
            assert(() => effective > 0, _lt("The effective rate (%s) must must strictly greater than 0.", effective.toString()));
            assert(() => periods > 0, _lt("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: _lt("Number of payment periods for an investment."),
        args: args(`
  rate (number) ${_lt("The interest rate.")}
  payment_amount (number) ${_lt("The amount of each payment made.")}
  present_value (number) ${_lt("The current value of the annuity.")}
  future_value (number, default=${DEFAULT_FUTURE_VALUE}) ${_lt("The future value remaining after the final payment has been made.")}
  end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING}) ${_lt("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);
            const p = toNumber(paymentAmount);
            const pv = toNumber(presentValue);
            const fv = toNumber(futureValue);
            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) {
        let i = 0;
        return reduceNumbers(values, (acc, v) => {
            i++;
            return acc + v / (1 + r) ** i;
        }, startValue);
    }
    const NPV = {
        description: _lt("The net present value of an investment based on a series of periodic cash flows and a discount rate."),
        args: args(`
  discount (number) ${_lt("The discount rate of the investment over one period.")}
  cashflow1 (number, range<number>) ${_lt("The first future cash flow.")}
  cashflow2 (number, range<number>, repeating) ${_lt("Additional future cash flows.")}
  `),
        returns: ["NUMBER"],
        // to do: replace by dollar format
        computeFormat: () => "#,##0.00",
        compute: function (discount, ...values) {
            const _discount = toNumber(discount);
            assert(() => _discount !== -1, _lt("The discount (%s) must be different from -1.", _discount.toString()));
            return npvResult(_discount, 0, values);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // PDURATION
    // -----------------------------------------------------------------------------
    const PDURATION = {
        description: _lt("Computes the number of periods needed for an investment to reach a value."),
        args: args(`
  rate (number) ${_lt("The rate at which the investment grows each period.")}
  present_value (number) ${_lt("The investment's current value.")}
  future_value (number) ${_lt("The investment's desired future value.")}
  `),
        returns: ["NUMBER"],
        compute: function (rate, presentValue, futureValue) {
            const _rate = toNumber(rate);
            const _presentValue = toNumber(presentValue);
            const _futureValue = toNumber(futureValue);
            assertRateStrictlyPositive(_rate);
            assert(() => _presentValue > 0, _lt("The present_value (%s) must be strictly positive.", _presentValue.toString()));
            assert(() => _futureValue > 0, _lt("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: _lt("Periodic payment for an annuity investment."),
        args: args(`
  rate (number) ${_lt("The annualized rate of interest.")}
  number_of_periods (number) ${_lt("The number of payments to be made.")}
  present_value (number) ${_lt("The current value of the annuity.")}
  future_value (number, default=${DEFAULT_FUTURE_VALUE}) ${_lt("The future value remaining after the final payment has been made.")}
  end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING}) ${_lt("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);
            const r = toNumber(rate);
            const t = toBoolean(endOrBeginning) ? 1 : 0;
            let fv = toNumber(futureValue);
            let pv = toNumber(presentValue);
            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: _lt("Payment on the principal of an investment."),
        args: args(`
  rate (number) ${_lt("The annualized rate of interest.")}
  period (number) ${_lt("The amortization period, in terms of number of periods.")}
  number_of_periods (number) ${_lt("The number of payments to be made.")}
  present_value (number) ${_lt("The current value of the annuity.")}
  future_value (number, default=${DEFAULT_FUTURE_VALUE}) ${_lt("The future value remaining after the final payment has been made.")}
  end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING}) ${_lt("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);
            const r = toNumber(rate);
            const period = toNumber(currentPeriod);
            const type = toBoolean(endOrBeginning) ? 1 : 0;
            const fv = toNumber(futureValue);
            const pv = toNumber(presentValue);
            assertNumberOfPeriodsStrictlyPositive(n);
            assert(() => period > 0 && period <= n, _lt("The period must be between 1 and number_of_periods", n.toString()));
            const payment = PMT.compute(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(r, eqPeriod, payment, eqPv, 0);
            const currentInterest = capitalAtPeriod * r;
            return payment + currentInterest;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // PV
    // -----------------------------------------------------------------------------
    const PV = {
        description: _lt("Present value of an annuity investment."),
        args: args(`
  rate (number) ${_lt("The interest rate.")}
  number_of_periods (number) ${_lt("The number of payments to be made.")}
  payment_amount (number) ${_lt("The amount per period to be paid.")}
  future_value (number, default=${DEFAULT_FUTURE_VALUE}) ${_lt("The future value remaining after the final payment has been made.")}
  end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING}) ${_lt("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);
            const n = toNumber(numberOfPeriods);
            const p = toNumber(paymentAmount);
            const fv = toNumber(futureValue);
            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: _lt("Price of a security paying periodic interest."),
        args: args(`
      settlement (date) ${_lt("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")}
      maturity (date) ${_lt("The maturity or end date of the security, when it can be redeemed at face, or par value.")}
      rate (number) ${_lt("The annualized rate of interest.")}
      yield (number) ${_lt("The expected annual yield of the security.")}
      redemption (number) ${_lt("The redemption amount per 100 face value, or par.")}
      frequency (number) ${_lt("The number of interest or coupon payments per year (1, 2, or 4).")}
      day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("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));
            const _maturity = Math.trunc(toNumber(maturity));
            const _rate = toNumber(rate);
            const _yield = toNumber(securityYield);
            const _redemption = toNumber(redemption);
            const _frequency = Math.trunc(toNumber(frequency));
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention));
            assertMaturityAndSettlementDatesAreValid(_settlement, _maturity);
            assertCouponFrequencyIsValid(_frequency);
            assertDayCountConventionIsValid(_dayCountConvention);
            assert(() => _rate >= 0, _lt("The rate (%s) must be positive or null.", _rate.toString()));
            assert(() => _yield >= 0, _lt("The yield (%s) must be positive or null.", _yield.toString()));
            assertRedemptionStrictlyPositive(_redemption);
            const years = YEARFRAC.compute(_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: _lt("Price of a discount security."),
        args: args(`
      settlement (date) ${_lt("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")}
      maturity (date) ${_lt("The maturity or end date of the security, when it can be redeemed at face, or par value.")}
      discount (number) ${_lt("The discount rate of the security at time of purchase.")}
      redemption (number) ${_lt("The redemption amount per 100 face value, or par.")}
      day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("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));
            const _maturity = Math.trunc(toNumber(maturity));
            const _discount = toNumber(discount);
            const _redemption = toNumber(redemption);
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention));
            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(_settlement, _maturity, _dayCountConvention);
            return _redemption - _discount * _redemption * yearsFrac;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // PRICEMAT
    // -----------------------------------------------------------------------------
    const PRICEMAT = {
        description: _lt("Calculates the price of a security paying interest at maturity, based on expected yield."),
        args: args(`
      settlement (date) ${_lt("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")}
      maturity (date) ${_lt("The maturity or end date of the security, when it can be redeemed at face, or par value.")}
      issue (date) ${_lt("The date the security was initially issued.")}
      rate (number) ${_lt("The annualized rate of interest.")}
      yield (number) ${_lt("The expected annual yield of the security.")}
      day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("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));
            const _maturity = Math.trunc(toNumber(maturity));
            const _issue = Math.trunc(toNumber(issue));
            const _rate = toNumber(rate);
            const _yield = toNumber(securityYield);
            const _dayCount = Math.trunc(toNumber(dayCountConvention));
            assertSettlementAndIssueDatesAreValid(_settlement, _issue);
            assertMaturityAndSettlementDatesAreValid(_settlement, _maturity);
            assertDayCountConventionIsValid(_dayCount);
            assert(() => _rate >= 0, _lt("The rate (%s) must be positive or null.", _rate.toString()));
            assert(() => _yield >= 0, _lt("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(_settlement, _maturity, _dayCount);
            const issueToSettlement = YEARFRAC.compute(_settlement, _issue, _dayCount);
            const issueToMaturity = YEARFRAC.compute(_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: _lt("Interest rate of an annuity investment."),
        args: args(`
  number_of_periods (number) ${_lt("The number of payments to be made.")}
  payment_per_period (number) ${_lt("The amount per period to be paid.")}
  present_value (number) ${_lt("The current value of the annuity.")}
  future_value (number, default=${DEFAULT_FUTURE_VALUE}) ${_lt("The future value remaining after the final payment has been made.")}
  end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING}) ${_lt("Whether payments are due at the end (0) or beginning (1) of each period.")}
  rate_guess (number, default=${RATE_GUESS_DEFAULT}) ${_lt("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);
            const payment = toNumber(paymentPerPeriod);
            const type = toBoolean(endOrBeginning) ? 1 : 0;
            const guess = toNumber(rateGuess);
            let fv = toNumber(futureValue);
            let pv = toNumber(presentValue);
            assertNumberOfPeriodsStrictlyPositive(n);
            assert(() => [payment, pv, fv].some((val) => val > 0) && [payment, pv, fv].some((val) => val < 0), _lt("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: _lt("Amount received at maturity for a security."),
        args: args(`
      settlement (date) ${_lt("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")}
      maturity (date) ${_lt("The maturity or end date of the security, when it can be redeemed at face, or par value.")}
      investment (number) ${_lt("The amount invested (irrespective of face value of each security).")}
      discount (number) ${_lt("The discount rate of the security invested in.")}
      day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("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));
            const _maturity = Math.trunc(toNumber(maturity));
            const _investment = toNumber(investment);
            const _discount = toNumber(discount);
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention));
            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(_settlement, _maturity, _dayCountConvention);
            return _investment / (1 - _discount * yearsFrac);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // RRI
    // -----------------------------------------------------------------------------
    const RRI = {
        description: _lt("Computes the rate needed for an investment to reach a specific value within a specific number of periods."),
        args: args(`
      number_of_periods (number) ${_lt("The number of periods.")}
      present_value (number) ${_lt("The present value of the investment.")}
      future_value (number) ${_lt("The future value of the investment.")}
    `),
        returns: ["NUMBER"],
        compute: function (numberOfPeriods, presentValue, futureValue) {
            const n = toNumber(numberOfPeriods);
            const pv = toNumber(presentValue);
            const fv = toNumber(futureValue);
            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: _lt("Depreciation of an asset using the straight-line method."),
        args: args(`
        cost (number) ${_lt("The initial cost of the asset.")}
        salvage (number) ${_lt("The value of the asset at the end of depreciation.")}
        life (number) ${_lt("The number of periods over which the asset is depreciated.")}
    `),
        returns: ["NUMBER"],
        computeFormat: () => "#,##0.00",
        compute: function (cost, salvage, life) {
            const _cost = toNumber(cost);
            const _salvage = toNumber(salvage);
            const _life = toNumber(life);
            // 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: _lt("Depreciation via sum of years digit method."),
        args: args(`
        cost (number) ${_lt("The initial cost of the asset.")}
        salvage (number) ${_lt("The value of the asset at the end of depreciation.")}
        life (number) ${_lt("The number of periods over which the asset is depreciated.")}
        period (number) ${_lt("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);
            const _salvage = toNumber(salvage);
            const _life = toNumber(life);
            const _period = toNumber(period);
            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: _lt("Price of a US Treasury bill."),
        args: args(`
      settlement (date) ${_lt("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")}
      maturity (date) ${_lt("The maturity or end date of the security, when it can be redeemed at face, or par value.")}
      discount (number) ${_lt("The discount rate of the bill at time of purchase.")}
    `),
        returns: ["NUMBER"],
        compute: function (settlement, maturity, discount) {
            const start = Math.trunc(toNumber(settlement));
            const end = Math.trunc(toNumber(maturity));
            const disc = toNumber(discount);
            assertMaturityAndSettlementDatesAreValid(start, end);
            assertSettlementLessThanOneYearBeforeMaturity(start, end);
            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(start, end, 2);
            return 100 * (1 - disc * yearFrac);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // TBILLEQ
    // -----------------------------------------------------------------------------
    const TBILLEQ = {
        description: _lt("Equivalent rate of return for a US Treasury bill."),
        args: args(`
      settlement (date) ${_lt("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")}
      maturity (date) ${_lt("The maturity or end date of the security, when it can be redeemed at face, or par value.")}
      discount (number) ${_lt("The discount rate of the bill at time of purchase.")}
    `),
        returns: ["NUMBER"],
        compute: function (settlement, maturity, discount) {
            const start = Math.trunc(toNumber(settlement));
            const end = Math.trunc(toNumber(maturity));
            const disc = toNumber(discount);
            assertMaturityAndSettlementDatesAreValid(start, end);
            assertSettlementLessThanOneYearBeforeMaturity(start, end);
            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(end, start);
            if (nDays <= 182) {
                return (365 * disc) / (360 - disc * nDays);
            }
            const p = TBILLPRICE.compute(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: _lt("The yield of a US Treasury bill based on price."),
        args: args(`
      settlement (date) ${_lt("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")}
      maturity (date) ${_lt("The maturity or end date of the security, when it can be redeemed at face, or par value.")}
      price (number) ${_lt("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));
            const end = Math.trunc(toNumber(maturity));
            const p = toNumber(price);
            assertMaturityAndSettlementDatesAreValid(start, end);
            assertSettlementLessThanOneYearBeforeMaturity(start, end);
            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(start, end, 2);
            return ((100 - p) / p) * (1 / yearFrac);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // VDB
    // -----------------------------------------------------------------------------
    const DEFAULT_VDB_NO_SWITCH = false;
    const VDB = {
        description: _lt("Variable declining balance. WARNING : does not handle decimal periods."),
        args: args(`
        cost (number) ${_lt("The initial cost of the asset.")}
        salvage (number) ${_lt("The value of the asset at the end of depreciation.")}
        life (number) ${_lt("The number of periods over which the asset is depreciated.")}
        start (number) ${_lt("Starting period to calculate depreciation.")}
        end (number) ${_lt("Ending period to calculate depreciation.")}
        factor (number, default=${DEFAULT_DDB_DEPRECIATION_FACTOR}) ${_lt("The number of months in the first year of depreciation.")}
  no_switch (number, default=${DEFAULT_VDB_NO_SWITCH}) ${_lt("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);
            const _salvage = toNumber(salvage);
            const _life = toNumber(life);
            /* 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));
            const _endPeriod = Math.trunc(toNumber(endPeriod));
            const _factor = toNumber(factor);
            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: _lt("Internal rate of return given non-periodic cash flows."),
        args: args(`
  cashflow_amounts (range<number>) ${_lt("An range containing the income or payments associated with the investment.")}
  cashflow_dates (range<number>) ${_lt("An range with dates corresponding to the cash flows in cashflow_amounts.")}
  rate_guess (number, default=${RATE_GUESS_DEFAULT}) ${_lt("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);
            const _cashFlows = cashflowAmounts.flat().map(toNumber);
            const _dates = cashflowDates.flat().map(toNumber);
            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: _lt("Net present value given to non-periodic cash flows.."),
        args: args(`
  discount (number) ${_lt("The discount rate of the investment over one period.")}
  cashflow_amounts (number, range<number>) ${_lt("An range containing the income or payments associated with the investment.")}
  cashflow_dates (number, range<number>) ${_lt("An range with dates corresponding to the cash flows in cashflow_amounts.")}
  `),
        returns: ["NUMBER"],
        compute: function (discount, cashflowAmounts, cashflowDates) {
            const rate = toNumber(discount);
            const _cashFlows = Array.isArray(cashflowAmounts)
                ? cashflowAmounts.flat().map(strictToNumber)
                : [strictToNumber(cashflowAmounts)];
            const _dates = Array.isArray(cashflowDates)
                ? cashflowDates.flat().map(strictToNumber)
                : [strictToNumber(cashflowDates)];
            if (Array.isArray(cashflowDates) && Array.isArray(cashflowAmounts)) {
                assertCashFlowsAndDatesHaveSameDimension(cashflowAmounts, cashflowDates);
            }
            else {
                assert(() => _cashFlows.length === _dates.length, _lt("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: _lt("Annual yield of a security paying periodic interest."),
        args: args(`
        settlement (date) ${_lt("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")}
        maturity (date) ${_lt("The maturity or end date of the security, when it can be redeemed at face, or par value.")}
        rate (number) ${_lt("The annualized rate of interest.")}
        price (number) ${_lt("The price at which the security is bought per 100 face value.")}
        redemption (number) ${_lt("The redemption amount per 100 face value, or par.")}
        frequency (number) ${_lt("The number of interest or coupon payments per year (1, 2, or 4).")}
        day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("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));
            const _maturity = Math.trunc(toNumber(maturity));
            const _rate = toNumber(rate);
            const _price = toNumber(price);
            const _redemption = toNumber(redemption);
            const _frequency = Math.trunc(toNumber(frequency));
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention));
            assertMaturityAndSettlementDatesAreValid(_settlement, _maturity);
            assertCouponFrequencyIsValid(_frequency);
            assertDayCountConventionIsValid(_dayCountConvention);
            assert(() => _rate >= 0, _lt("The rate (%s) must be positive or null.", _rate.toString()));
            assertPriceStrictlyPositive(_price);
            assertRedemptionStrictlyPositive(_redemption);
            const years = YEARFRAC.compute(_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: _lt("Annual yield of a discount security."),
        args: args(`
        settlement (date) ${_lt("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")}
        maturity (date) ${_lt("The maturity or end date of the security, when it can be redeemed at face, or par value.")}
        price (number) ${_lt("The price at which the security is bought per 100 face value.")}
        redemption (number) ${_lt("The redemption amount per 100 face value, or par.")}
        day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("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));
            const _maturity = Math.trunc(toNumber(maturity));
            const _price = toNumber(price);
            const _redemption = toNumber(redemption);
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention));
            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(settlement, maturity, dayCountConvention);
            return (_redemption / _price - 1) / yearFrac;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // YIELDMAT
    // -----------------------------------------------------------------------------
    const YIELDMAT = {
        description: _lt("Annual yield of a security paying interest at maturity."),
        args: args(`
        settlement (date) ${_lt("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")}
        maturity (date) ${_lt("The maturity or end date of the security, when it can be redeemed at face, or par value.")}
        issue (date) ${_lt("The date the security was initially issued.")}
        rate (number) ${_lt("The annualized rate of interest.")}
        price (number) ${_lt("The price at which the security is bought.")}
        day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("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));
            const _maturity = Math.trunc(toNumber(maturity));
            const _issue = Math.trunc(toNumber(issue));
            const _rate = toNumber(rate);
            const _price = toNumber(price);
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention));
            assertMaturityAndSettlementDatesAreValid(_settlement, _maturity);
            assertDayCountConventionIsValid(_dayCountConvention);
            assert(() => _settlement >= _issue, _lt("The settlement (%s) must be greater than or equal to the issue (%s).", _settlement.toString(), _issue.toString()));
            assert(() => _rate >= 0, _lt("The rate (%s) must be positive or null.", _rate.toString()));
            assertPriceStrictlyPositive(_price);
            const issueToMaturity = YEARFRAC.compute(_issue, _maturity, _dayCountConvention);
            const issueToSettlement = YEARFRAC.compute(_issue, _settlement, _dayCountConvention);
            const settlementToMaturity = YEARFRAC.compute(_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,
        COUPDAYS: COUPDAYS,
        COUPDAYBS: COUPDAYBS,
        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,
        PV: PV,
        PRICE: PRICE,
        PRICEDISC: PRICEDISC,
        PRICEMAT: PRICEMAT,
        RATE: RATE,
        RECEIVED: RECEIVED,
        RRI: RRI,
        SLN: SLN,
        SYD: SYD,
        TBILLPRICE: TBILLPRICE,
        TBILLEQ: TBILLEQ,
        TBILLYIELD: TBILLYIELD,
        VDB: VDB,
        XIRR: XIRR,
        XNPV: XNPV,
        YIELD: YIELD,
        YIELDDISC: YIELDDISC,
        YIELDMAT: YIELDMAT
    });

    // -----------------------------------------------------------------------------
    // ISERR
    // -----------------------------------------------------------------------------
    const ISERR = {
        description: _lt("Whether a value is an error other than #N/A."),
        args: args(`value (any, lazy) ${_lt("The value to be verified as an error type.")}`),
        returns: ["BOOLEAN"],
        compute: function (value) {
            try {
                value();
                return false;
            }
            catch (e) {
                return (e === null || e === void 0 ? void 0 : e.errorType) != CellErrorType.NotAvailable;
            }
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ISERROR
    // -----------------------------------------------------------------------------
    const ISERROR = {
        description: _lt("Whether a value is an error."),
        args: args(`value (any, lazy) ${_lt("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: _lt("Whether a value is `true` or `false`."),
        args: args(`value (any, lazy) ${_lt("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: _lt("Whether a value is the error #N/A."),
        args: args(`value (any, lazy) ${_lt("The value to be verified as an error type.")}`),
        returns: ["BOOLEAN"],
        compute: function (value) {
            try {
                value();
                return false;
            }
            catch (e) {
                return (e === null || e === void 0 ? void 0 : e.errorType) == CellErrorType.NotAvailable;
            }
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ISNONTEXT
    // -----------------------------------------------------------------------------
    const ISNONTEXT = {
        description: _lt("Whether a value is non-textual."),
        args: args(`value (any, lazy) ${_lt("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: _lt("Whether a value is a number."),
        args: args(`value (any, lazy) ${_lt("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: _lt("Whether a value is text."),
        args: args(`value (any, lazy) ${_lt("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: _lt("Whether the referenced cell is empty"),
        args: args(`value (any, lazy) ${_lt("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: _lt("Returns the error value #N/A."),
        args: args(``),
        returns: ["BOOLEAN"],
        compute: function (value) {
            throw new NotAvailableError();
        },
        isExported: true,
    };

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

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

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

    const DEFAULT_IS_SORTED = true;
    const DEFAULT_MATCH_MODE = 0;
    const DEFAULT_SEARCH_MODE = 1;
    // -----------------------------------------------------------------------------
    // COLUMN
    // -----------------------------------------------------------------------------
    const COLUMN = {
        description: _lt("Column number of a specified cell."),
        args: args(`cell_reference (meta, default=${_lt("The cell in which the formula is entered")}) ${_lt("The cell whose column number will be returned. Column A corresponds to 1.")}
    `),
        returns: ["NUMBER"],
        compute: function (cellReference) {
            var _a;
            const _cellReference = cellReference || ((_a = this.__originCellXC) === null || _a === void 0 ? void 0 : _a.call(this));
            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: _lt("Number of columns in a specified array or range."),
        args: args(`range (meta) ${_lt("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: _lt(`Horizontal lookup`),
        args: args(`
      search_key (any) ${_lt("The value to search for. For example, 42, 'Cats', or I24.")}
      range (range) ${_lt("The range to consider for the search. The first row in the range is searched for the key specified in search_key.")}
      index (number) ${_lt("The row index of the value to be returned, where the first row in range is numbered 1.")}
      is_sorted (boolean, default=${DEFAULT_IS_SORTED}) ${_lt("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"],
        compute: function (searchKey, range, index, isSorted = DEFAULT_IS_SORTED) {
            const _index = Math.trunc(toNumber(index));
            const _searchKey = normalizeValue(searchKey);
            assert(() => 1 <= _index && _index <= range[0].length, _lt("[[FUNCTION_NAME]] evaluates to an out of bounds range."));
            const _isSorted = toBoolean(isSorted);
            let colIndex;
            if (_isSorted) {
                colIndex = dichotomicSearch(range, _searchKey, "nextSmaller", "asc", range.length, getNormalizedValueFromRowRange);
            }
            else {
                colIndex = linearSearch(range, _searchKey, "strict", range.length, getNormalizedValueFromRowRange);
            }
            assert(() => colIndex > -1, _lt("Did not find value '%s' in [[FUNCTION_NAME]] evaluation.", toString(searchKey)));
            return range[colIndex][_index - 1];
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // INDEX
    // -----------------------------------------------------------------------------
    const INDEX = {
        description: _lt(`Returns the content of a cell, specified by row and column offset.`),
        args: args(`
      reference (any, range) ${_lt("The range of cells from which the value is returned.")}
      row (number) ${_lt("The index of the row to be returned from within the reference range of cells.")}
      column (number) ${_lt("The index of the column to be returned from within the reference range of cells.")}
`),
        returns: ["ANY"],
        computeFormat: (reference, row, column) => {
            var _a;
            const _row = toNumber(row.value);
            const _column = toNumber(column.value);
            return (_a = reference[_column - 1][_row - 1]) === null || _a === void 0 ? void 0 : _a.format;
        },
        compute: function (reference, row, column) {
            const _reference = isMatrix(reference) ? reference : [[reference]];
            const _row = toNumber(row);
            const _column = toNumber(column);
            assert(() => _column >= 0 &&
                _column - 1 < _reference.length &&
                _row >= 0 &&
                _row - 1 < _reference[0].length, _lt("Index out of range."));
            assert(() => row !== 0 && column !== 0, _lt("This function can only return a single cell value, not an array. Provide valid row and column indices."));
            return _reference[_column - 1][_row - 1];
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // LOOKUP
    // -----------------------------------------------------------------------------
    const LOOKUP = {
        description: _lt(`Look up a value.`),
        args: args(`
      search_key (any) ${_lt("The value to search for. For example, 42, 'Cats', or I24.")}
      search_array (range) ${_lt("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.")}
      result_range (range, optional) ${_lt("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"],
        compute: function (searchKey, searchArray, resultRange) {
            let nbCol = searchArray.length;
            let nbRow = searchArray[0].length;
            const _searchKey = normalizeValue(searchKey);
            const verticalSearch = nbRow >= nbCol;
            const getElement = verticalSearch
                ? getNormalizedValueFromColumnRange
                : getNormalizedValueFromRowRange;
            const rangeLength = verticalSearch ? nbRow : nbCol;
            const index = dichotomicSearch(searchArray, _searchKey, "nextSmaller", "asc", rangeLength, getElement);
            assert(() => index >= 0, _lt("Did not find value '%s' in [[FUNCTION_NAME]] evaluation.", toString(searchKey)));
            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, _lt("The result_range must be a single row or a single column."));
            if (nbCol > 1) {
                assert(() => index <= nbCol - 1, _lt("[[FUNCTION_NAME]] evaluates to an out of range row value %s.", (index + 1).toString()));
                return resultRange[index][0];
            }
            assert(() => index <= nbRow - 1, _lt("[[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: _lt(`Position of item in range that matches value.`),
        args: args(`
      search_key (any) ${_lt("The value to search for. For example, 42, 'Cats', or I24.")}
      range (any, range) ${_lt("The one-dimensional array to be searched.")}
      search_type (number, default=${DEFAULT_SEARCH_TYPE}) ${_lt("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);
            const _searchKey = normalizeValue(searchKey);
            const nbCol = range.length;
            const nbRow = range[0].length;
            assert(() => nbCol === 1 || nbRow === 1, _lt("The range must be a single row or a single column."));
            let index = -1;
            const getElement = nbCol === 1 ? getNormalizedValueFromColumnRange : getNormalizedValueFromRowRange;
            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;
            }
            assert(() => index >= 0, _lt("Did not find value '%s' in [[FUNCTION_NAME]] evaluation.", toString(searchKey)));
            return index + 1;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ROW
    // -----------------------------------------------------------------------------
    const ROW = {
        description: _lt("Row number of a specified cell."),
        args: args(`cell_reference (meta, default=${_lt("The cell in which the formula is entered by default")}) ${_lt("The cell whose row number will be returned.")}`),
        returns: ["NUMBER"],
        compute: function (cellReference) {
            var _a;
            cellReference = cellReference || ((_a = this.__originCellXC) === null || _a === void 0 ? void 0 : _a.call(this));
            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: _lt("Number of rows in a specified array or range."),
        args: args(`range (meta) ${_lt("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: _lt(`Vertical lookup.`),
        args: args(`
      search_key (any) ${_lt("The value to search for. For example, 42, 'Cats', or I24.")}
      range (any, range) ${_lt("The range to consider for the search. The first column in the range is searched for the key specified in search_key.")}
      index (number) ${_lt("The column index of the value to be returned, where the first column in range is numbered 1.")}
      is_sorted (boolean, default=${DEFAULT_IS_SORTED}) ${_lt("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"],
        compute: function (searchKey, range, index, isSorted = DEFAULT_IS_SORTED) {
            const _index = Math.trunc(toNumber(index));
            const _searchKey = normalizeValue(searchKey);
            assert(() => 1 <= _index && _index <= range.length, _lt("[[FUNCTION_NAME]] evaluates to an out of bounds range."));
            const _isSorted = toBoolean(isSorted);
            let rowIndex;
            if (_isSorted) {
                rowIndex = dichotomicSearch(range, _searchKey, "nextSmaller", "asc", range[0].length, getNormalizedValueFromColumnRange);
            }
            else {
                rowIndex = linearSearch(range, _searchKey, "strict", range[0].length, getNormalizedValueFromColumnRange);
            }
            assert(() => rowIndex > -1, _lt("Did not find value '%s' in [[FUNCTION_NAME]] evaluation.", toString(searchKey)));
            return range[_index - 1][rowIndex];
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // XLOOKUP
    // -----------------------------------------------------------------------------
    const XLOOKUP = {
        description: _lt(`Search a range for a match and return the corresponding item from a second range.`),
        args: args(`
      search_key (any) ${_lt("The value to search for.")}
      lookup_range (any, range) ${_lt("The range to consider for the search. Should be a single column or a single row.")}
      return_range (any, range) ${_lt("The range containing the return value. Should have the same dimensions as lookup_range.")}
      if_not_found (any, lazy, optional) ${_lt("If a valid match is not found, return this value.")}
      match_mode (any, default=${DEFAULT_MATCH_MODE}) ${_lt("(0) Exact match. (-1) Return next smaller item if no match. (1) Return next greater item if no match.")}
      search_mode (any, default=${DEFAULT_SEARCH_MODE}) ${_lt("(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"],
        compute: function (searchKey, lookupRange, returnRange, defaultValue, matchMode = DEFAULT_MATCH_MODE, searchMode = DEFAULT_SEARCH_MODE) {
            const _matchMode = Math.trunc(toNumber(matchMode));
            const _searchMode = Math.trunc(toNumber(searchMode));
            const _searchKey = normalizeValue(searchKey);
            assert(() => lookupRange.length === 1 || lookupRange[0].length === 1, _lt("lookup_range should be either a single row or single column."));
            assert(() => returnRange.length === 1 || returnRange[0].length === 1, _lt("return_range should be either a single row or single column."));
            assert(() => returnRange.length === lookupRange.length &&
                returnRange[0].length === lookupRange[0].length, _lt("return_range should have the same dimensions as lookup_range."));
            assert(() => [-1, 1, -2, 2].includes(_searchMode), _lt("searchMode should be a value in [-1, 1, -2, 2]."));
            assert(() => [-1, 0, 1].includes(_matchMode), _lt("matchMode should be a value in [-1, 0, 1]."));
            const getElement = lookupRange.length === 1 ? getNormalizedValueFromColumnRange : getNormalizedValueFromRowRange;
            const rangeLen = lookupRange.length === 1 ? lookupRange[0].length : lookupRange.length;
            const mode = _matchMode === 0 ? "strict" : _matchMode === 1 ? "nextGreater" : "nextSmaller";
            const reverseSearch = _searchMode === -1;
            let index;
            if (_searchMode === 2 || _searchMode === -2) {
                const sortOrder = _searchMode === 2 ? "asc" : "desc";
                index = dichotomicSearch(lookupRange, _searchKey, mode, sortOrder, rangeLen, getElement);
            }
            else {
                index = linearSearch(lookupRange, _searchKey, mode, rangeLen, getElement, reverseSearch);
            }
            if (index !== -1) {
                return (lookupRange.length === 1 ? returnRange[0][index] : returnRange[index][0]);
            }
            const _defaultValue = defaultValue === null || defaultValue === void 0 ? void 0 : defaultValue();
            assert(() => !!_defaultValue, _lt("Did not find value '%s' in [[FUNCTION_NAME]] evaluation.", toString(searchKey)));
            return _defaultValue;
        },
        isExported: true,
    };

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

    // -----------------------------------------------------------------------------
    // ADD
    // -----------------------------------------------------------------------------
    const ADD = {
        description: _lt(`Sum of two numbers.`),
        args: args(`
      value1 (number) ${_lt("The first addend.")}
      value2 (number) ${_lt("The second addend.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (value1, value2) => (value1 === null || value1 === void 0 ? void 0 : value1.format) || (value2 === null || value2 === void 0 ? void 0 : value2.format),
        compute: function (value1, value2) {
            return toNumber(value1) + toNumber(value2);
        },
    };
    // -----------------------------------------------------------------------------
    // CONCAT
    // -----------------------------------------------------------------------------
    const CONCAT = {
        description: _lt(`Concatenation of two values.`),
        args: args(`
      value1 (string) ${_lt("The value to which value2 will be appended.")}
      value2 (string) ${_lt("The value to append to value1.")}
    `),
        returns: ["STRING"],
        compute: function (value1, value2) {
            return toString(value1) + toString(value2);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DIVIDE
    // -----------------------------------------------------------------------------
    const DIVIDE = {
        description: _lt(`One number divided by another.`),
        args: args(`
      dividend (number) ${_lt("The number to be divided.")}
      divisor (number) ${_lt("The number to divide by.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (dividend, divisor) => (dividend === null || dividend === void 0 ? void 0 : dividend.format) || (divisor === null || divisor === void 0 ? void 0 : divisor.format),
        compute: function (dividend, divisor) {
            const _divisor = toNumber(divisor);
            assert(() => _divisor !== 0, _lt("The divisor must be different from zero."));
            return toNumber(dividend) / _divisor;
        },
    };
    // -----------------------------------------------------------------------------
    // EQ
    // -----------------------------------------------------------------------------
    function isEmpty(value) {
        return value === null || value === undefined;
    }
    const getNeutral = { number: 0, string: "", boolean: false };
    function areAlmostEqual(value1, value2, epsilon = 2e-16) {
        return Math.abs(value1 - value2) < epsilon;
    }
    const EQ = {
        description: _lt(`Equal.`),
        args: args(`
      value1 (any) ${_lt("The first value.")}
      value2 (any) ${_lt("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();
            }
            if (typeof value1 === "number" && typeof value2 === "number") {
                return areAlmostEqual(value1, value2);
            }
            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: _lt(`Strictly greater than.`),
        args: args(`
      value1 (any) ${_lt("The value to test as being greater than value2.")}
      value2 (any) ${_lt("The second value.")}
    `),
        returns: ["BOOLEAN"],
        compute: function (value1, value2) {
            return applyRelationalOperator(value1, value2, (v1, v2) => {
                if (typeof v1 === "number" && typeof v2 === "number") {
                    return !areAlmostEqual(v1, v2) && v1 > v2;
                }
                return v1 > v2;
            });
        },
    };
    // -----------------------------------------------------------------------------
    // GTE
    // -----------------------------------------------------------------------------
    const GTE = {
        description: _lt(`Greater than or equal to.`),
        args: args(`
      value1 (any) ${_lt("The value to test as being greater than or equal to value2.")}
      value2 (any) ${_lt("The second value.")}
    `),
        returns: ["BOOLEAN"],
        compute: function (value1, value2) {
            return applyRelationalOperator(value1, value2, (v1, v2) => {
                if (typeof v1 === "number" && typeof v2 === "number") {
                    return areAlmostEqual(v1, v2) || v1 > v2;
                }
                return v1 >= v2;
            });
        },
    };
    // -----------------------------------------------------------------------------
    // LT
    // -----------------------------------------------------------------------------
    const LT = {
        description: _lt(`Less than.`),
        args: args(`
      value1 (any) ${_lt("The value to test as being less than value2.")}
      value2 (any) ${_lt("The second value.")}
    `),
        returns: ["BOOLEAN"],
        compute: function (value1, value2) {
            return !GTE.compute(value1, value2);
        },
    };
    // -----------------------------------------------------------------------------
    // LTE
    // -----------------------------------------------------------------------------
    const LTE = {
        description: _lt(`Less than or equal to.`),
        args: args(`
      value1 (any) ${_lt("The value to test as being less than or equal to value2.")}
      value2 (any) ${_lt("The second value.")}
    `),
        returns: ["BOOLEAN"],
        compute: function (value1, value2) {
            return !GT.compute(value1, value2);
        },
    };
    // -----------------------------------------------------------------------------
    // MINUS
    // -----------------------------------------------------------------------------
    const MINUS = {
        description: _lt(`Difference of two numbers.`),
        args: args(`
      value1 (number) ${_lt("The minuend, or number to be subtracted from.")}
      value2 (number) ${_lt("The subtrahend, or number to subtract from value1.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (value1, value2) => (value1 === null || value1 === void 0 ? void 0 : value1.format) || (value2 === null || value2 === void 0 ? void 0 : value2.format),
        compute: function (value1, value2) {
            return toNumber(value1) - toNumber(value2);
        },
    };
    // -----------------------------------------------------------------------------
    // MULTIPLY
    // -----------------------------------------------------------------------------
    const MULTIPLY = {
        description: _lt(`Product of two numbers`),
        args: args(`
      factor1 (number) ${_lt("The first multiplicand.")}
      factor2 (number) ${_lt("The second multiplicand.")}
    `),
        returns: ["NUMBER"],
        computeFormat: (factor1, factor2) => (factor1 === null || factor1 === void 0 ? void 0 : factor1.format) || (factor2 === null || factor2 === void 0 ? void 0 : factor2.format),
        compute: function (factor1, factor2) {
            return toNumber(factor1) * toNumber(factor2);
        },
    };
    // -----------------------------------------------------------------------------
    // NE
    // -----------------------------------------------------------------------------
    const NE = {
        description: _lt(`Not equal.`),
        args: args(`
      value1 (any) ${_lt("The first value.")}
      value2 (any) ${_lt("The value to test against value1 for inequality.")}
    `),
        returns: ["BOOLEAN"],
        compute: function (value1, value2) {
            return !EQ.compute(value1, value2);
        },
    };
    // -----------------------------------------------------------------------------
    // POW
    // -----------------------------------------------------------------------------
    const POW = {
        description: _lt(`A number raised to a power.`),
        args: args(`
      base (number) ${_lt("The number to raise to the exponent power.")}
      exponent (number) ${_lt("The exponent to raise base to.")}
    `),
        returns: ["NUMBER"],
        compute: function (base, exponent) {
            return POWER.compute(base, exponent);
        },
    };
    // -----------------------------------------------------------------------------
    // UMINUS
    // -----------------------------------------------------------------------------
    const UMINUS = {
        description: _lt(`A number with the sign reversed.`),
        args: args(`
      value (number) ${_lt("The number to have its sign reversed. Equivalently, the number to multiply by -1.")}
    `),
        computeFormat: (value) => value === null || value === void 0 ? void 0 : value.format,
        returns: ["NUMBER"],
        compute: function (value) {
            return -toNumber(value);
        },
    };
    // -----------------------------------------------------------------------------
    // UNARY_PERCENT
    // -----------------------------------------------------------------------------
    const UNARY_PERCENT = {
        description: _lt(`Value interpreted as a percentage.`),
        args: args(`
      percentage (number) ${_lt("The value to interpret as a percentage.")}
    `),
        returns: ["NUMBER"],
        compute: function (percentage) {
            return toNumber(percentage) / 100;
        },
    };
    // -----------------------------------------------------------------------------
    // UPLUS
    // -----------------------------------------------------------------------------
    const UPLUS = {
        description: _lt(`A specified number, unchanged.`),
        args: args(`
      value (any) ${_lt("The number to return.")}
    `),
        returns: ["ANY"],
        computeFormat: (value) => value === null || value === void 0 ? void 0 : 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: _lt("Gets character associated with number."),
        args: args(`
      table_number (number) ${_lt("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));
            assert(() => _tableNumber >= 1, _lt("The table_number (%s) is out of range.", _tableNumber.toString()));
            return String.fromCharCode(_tableNumber);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // CLEAN
    // -----------------------------------------------------------------------------
    const CLEAN = {
        description: _lt("Remove non-printable characters from a piece of text."),
        args: args(`
      text (string) ${_lt("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: _lt("Appends strings to one another."),
        args: args(`
      string1 (string, range<string>) ${_lt("The initial string.")}
      string2 (string, range<string>, repeating) ${_lt("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: _lt("Tests whether two strings are identical."),
        args: args(`
      string1 (string) ${_lt("The first string to compare.")}
      string2 (string) ${_lt("The second string to compare.")}
  `),
        returns: ["BOOLEAN"],
        compute: function (string1, string2) {
            return toString(string1) === toString(string2);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // FIND
    // -----------------------------------------------------------------------------
    const FIND = {
        description: _lt("First position of string found in text, case-sensitive."),
        args: args(`
      search_for (string) ${_lt("The string to look for within text_to_search.")}
      text_to_search (string) ${_lt("The text to search for the first occurrence of search_for.")}
      starting_at (number, default=${DEFAULT_STARTING_AT}) ${_lt("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);
            assert(() => _textToSearch !== "", _lt(`The text_to_search must be non-empty.`));
            assert(() => _startingAt >= 1, _lt("The starting_at (%s) must be greater than or equal to 1.", _startingAt.toString()));
            const result = _textToSearch.indexOf(_searchFor, _startingAt - 1);
            assert(() => result >= 0, _lt("In [[FUNCTION_NAME]] evaluation, cannot find '%s' within '%s'.", _searchFor.toString(), _textToSearch));
            return result + 1;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // JOIN
    // -----------------------------------------------------------------------------
    const JOIN = {
        description: _lt("Concatenates elements of arrays with delimiter."),
        args: args(`
      delimiter (string) ${_lt("The character or string to place between each concatenated value.")}
      value_or_array1 (string, range<string>) ${_lt("The value or values to be appended using delimiter.")}
      value_or_array2 (string, range<string>, repeating) ${_lt("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: _lt("Substring from beginning of specified string."),
        args: args(`
      text (string) ${_lt("The string from which the left portion will be returned.")}
      number_of_characters (number, optional) ${_lt("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]) : 1;
            assert(() => _numberOfCharacters >= 0, _lt("The number_of_characters (%s) must be positive or null.", _numberOfCharacters.toString()));
            return toString(text).substring(0, _numberOfCharacters);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // LEN
    // -----------------------------------------------------------------------------
    const LEN = {
        description: _lt("Length of a string."),
        args: args(`
      text (string) ${_lt("The string whose length will be returned.")}
  `),
        returns: ["NUMBER"],
        compute: function (text) {
            return toString(text).length;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // LOWER
    // -----------------------------------------------------------------------------
    const LOWER = {
        description: _lt("Converts a specified string to lowercase."),
        args: args(`
      text (string) ${_lt("The string to convert to lowercase.")}
  `),
        returns: ["STRING"],
        compute: function (text) {
            return toString(text).toLowerCase();
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // MID
    // -----------------------------------------------------------------------------
    const MID = {
        description: _lt("A segment of a string."),
        args: args(`
      text (string) ${_lt("The string to extract a segment from.")}
      starting_at  (number) ${_lt("The index from the left of string from which to begin extracting. The first character in string has the index 1.")}
      extract_length  (number) ${_lt("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);
            const _extract_length = toNumber(extract_length);
            assert(() => _starting_at >= 1, _lt("The starting_at argument (%s) must be positive greater than one.", _starting_at.toString()));
            assert(() => _extract_length >= 0, _lt("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: _lt("Capitalizes each word in a specified string."),
        args: args(`
  text_to_capitalize (string) ${_lt("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: _lt("Replaces part of a text string with different text."),
        args: args(`
      text (string) ${_lt("The text, a part of which will be replaced.")}
      position (number) ${_lt("The position where the replacement will begin (starting from 1).")}
      length (number) ${_lt("The number of characters in the text to be replaced.")}
      new_text (string) ${_lt("The text which will be inserted into the original text.")}
  `),
        returns: ["STRING"],
        compute: function (text, position, length, newText) {
            const _position = toNumber(position);
            assert(() => _position >= 1, _lt("The position (%s) must be greater than or equal to 1.", _position.toString()));
            const _text = toString(text);
            const _length = toNumber(length);
            const _newText = toString(newText);
            return _text.substring(0, _position - 1) + _newText + _text.substring(_position - 1 + _length);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // RIGHT
    // -----------------------------------------------------------------------------
    const RIGHT = {
        description: _lt("A substring from the end of a specified string."),
        args: args(`
      text (string) ${_lt("The string from which the right portion will be returned.")}
      number_of_characters (number, optional) ${_lt("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]) : 1;
            assert(() => _numberOfCharacters >= 0, _lt("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: _lt("First position of string found in text, ignoring case."),
        args: args(`
      search_for (string) ${_lt("The string to look for within text_to_search.")}
      text_to_search (string) ${_lt("The text to search for the first occurrence of search_for.")}
      starting_at (number, default=${DEFAULT_STARTING_AT}) ${_lt("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);
            assert(() => _textToSearch !== "", _lt(`The text_to_search must be non-empty.`));
            assert(() => _startingAt >= 1, _lt("The starting_at (%s) must be greater than or equal to 1.", _startingAt.toString()));
            const result = _textToSearch.indexOf(_searchFor, _startingAt - 1);
            assert(() => result >= 0, _lt("In [[FUNCTION_NAME]] evaluation, cannot find '%s' within '%s'.", _searchFor, _textToSearch));
            return result + 1;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SUBSTITUTE
    // -----------------------------------------------------------------------------
    const SUBSTITUTE = {
        description: _lt("Replaces existing text with new text in a string."),
        args: args(`
      text_to_search (string) ${_lt("The text within which to search and replace.")}
      search_for (string) ${_lt("The string to search for within text_to_search.")}
      replace_with (string) ${_lt("The string that will replace search_for.")}
      occurrence_number (number, optional) ${_lt("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);
            assert(() => _occurrenceNumber >= 0, _lt("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: _lt("Combines text from multiple strings and/or arrays."),
        args: args(`
      delimiter (string) ${_lt(" A string, possible empty, or a reference to a valid string. If empty, the text will be simply concatenated.")}
      ignore_empty (boolean) ${_lt("A boolean; if TRUE, empty cells selected in the text arguments won't be included in the result.")}
      text1 (string, range<string>) ${_lt("Any text item. This could be a string, or an array of strings in a range.")}
      text2 (string, range<string>, repeating) ${_lt("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: _lt("Removes space characters."),
        args: args(`
      text (string) ${_lt("The text or reference to a cell containing text to be trimmed.")}
  `),
        returns: ["STRING"],
        compute: function (text) {
            return toString(text).trim();
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // UPPER
    // -----------------------------------------------------------------------------
    const UPPER = {
        description: _lt("Converts a specified string to uppercase."),
        args: args(`
      text (string) ${_lt("The string to convert to uppercase.")}
  `),
        returns: ["STRING"],
        compute: function (text) {
            return toString(text).toUpperCase();
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // TEXT
    // -----------------------------------------------------------------------------
    const TEXT = {
        description: _lt("Converts a number to text according to a specified format."),
        args: args(`
      number (number) ${_lt("The number, date or time to format.")}
      format (string) ${_lt("The pattern by which to format the number, enclosed in quotation marks.")}
  `),
        returns: ["STRING"],
        compute: function (number, format) {
            const _number = toNumber(number);
            return formatValue(_number, toString(format));
        },
        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,
        SUBSTITUTE: SUBSTITUTE,
        TEXTJOIN: TEXTJOIN,
        TRIM: TRIM,
        UPPER: UPPER,
        TEXT: TEXT
    });

    const functions$4 = {
        database,
        date,
        financial,
        info,
        lookup,
        logical,
        math,
        misc: misc$1,
        operators,
        statistical,
        text,
        engineering,
    };
    const functionNameRegex = /^[A-Z0-9\_\.]+$/;
    //------------------------------------------------------------------------------
    // Function registry
    //------------------------------------------------------------------------------
    class FunctionRegistry extends Registry {
        constructor() {
            super(...arguments);
            this.mapping = {};
        }
        add(name, addDescr) {
            name = name.toUpperCase();
            if (!name.match(functionNameRegex)) {
                throw new Error(_lt("Invalid function name %s. Function names can exclusively contain alphanumerical values separated by dots (.) or underscore (_)", name));
            }
            const descr = addMetaInfoFromArg(addDescr);
            validateArguments(descr.args);
            function computeValueAndFormat(...args) {
                const computeValue = descr.compute.bind(this);
                const computeFormat = descr.computeFormat ? descr.computeFormat.bind(this) : () => undefined;
                return {
                    value: computeValue(...extractArgValuesFromArgs(args)),
                    format: computeFormat(...args),
                };
            }
            this.mapping[name] = computeValueAndFormat;
            super.add(name, descr);
            return this;
        }
    }
    function extractArgValuesFromArgs(args) {
        return args.map((arg) => {
            if (arg === undefined) {
                return undefined;
            }
            if (typeof arg === "function") {
                return () => _extractArgValuesFromArgs(arg());
            }
            return _extractArgValuesFromArgs(arg);
        });
    }
    function _extractArgValuesFromArgs(arg) {
        if (Array.isArray(arg)) {
            return arg.map((col) => col.map((simpleArg) => simpleArg === null || simpleArg === void 0 ? void 0 : simpleArg.value));
        }
        return arg === null || arg === void 0 ? void 0 : arg.value;
    }
    const functionRegistry = new FunctionRegistry();
    for (let category in functions$4) {
        const fns = functions$4[category];
        for (let name in fns) {
            const addDescr = fns[name];
            addDescr.category = category;
            name = name.replace(/_/g, ".");
            functionRegistry.add(name, { isExported: false, ...addDescr });
        }
    }

    /**
     * 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 functions$3 = functionRegistry.content;
    const POSTFIX_UNARY_OPERATORS = ["%"];
    const OPERATORS = "+,-,*,/,:,=,<>,>=,>,<=,<,^,&".split(",").concat(POSTFIX_UNARY_OPERATORS);
    function tokenize(str) {
        const chars = str.split("");
        const result = [];
        while (chars.length) {
            let token = tokenizeSpace(chars) ||
                tokenizeMisc(chars) ||
                tokenizeOperator(chars) ||
                tokenizeString(chars) ||
                tokenizeDebugger(chars) ||
                tokenizeInvalidRange(chars) ||
                tokenizeNumber(chars) ||
                tokenizeSymbol(chars);
            if (!token) {
                token = { type: "UNKNOWN", value: chars.shift() };
            }
            result.push(token);
        }
        return result;
    }
    function tokenizeDebugger(chars) {
        if (chars[0] === "?") {
            chars.shift();
            return { type: "DEBUGGER", value: "?" };
        }
        return null;
    }
    const misc = {
        ",": "COMMA",
        "(": "LEFT_PAREN",
        ")": "RIGHT_PAREN",
    };
    function tokenizeMisc(chars) {
        if (chars[0] in misc) {
            const value = chars.shift();
            const type = misc[value];
            return { type, value };
        }
        return null;
    }
    function startsWith(chars, op) {
        for (let i = 0; i < op.length; i++) {
            if (op[i] !== chars[i]) {
                return false;
            }
        }
        return true;
    }
    function tokenizeOperator(chars) {
        for (let op of OPERATORS) {
            if (startsWith(chars, op)) {
                chars.splice(0, op.length);
                return { type: "OPERATOR", value: op };
            }
        }
        return null;
    }
    function tokenizeNumber(chars) {
        const match = concat(chars).match(formulaNumberRegexp);
        if (match) {
            chars.splice(0, match[0].length);
            return { type: "NUMBER", value: match[0] };
        }
        return null;
    }
    function tokenizeString(chars) {
        if (chars[0] === '"') {
            const startChar = chars.shift();
            let letters = startChar;
            while (chars[0] && (chars[0] !== startChar || letters[letters.length - 1] === "\\")) {
                letters += chars.shift();
            }
            if (chars[0] === '"') {
                letters += chars.shift();
            }
            return {
                type: "STRING",
                value: letters,
            };
        }
        return null;
    }
    /**
      - \p{L} is for any letter (from any language)
      - \p{N} is for any number
      - the u flag at the end is for unicode, which enables the `\p{...}` syntax
     */
    const unicodeSymbolCharRegexp = /\p{L}|\p{N}|_|\.|!|\$/u;
    const SYMBOL_CHARS = new Set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.!$");
    /**
     * 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[0] === "'") {
            let lastChar = chars.shift();
            result += lastChar;
            while (chars[0]) {
                lastChar = chars.shift();
                result += lastChar;
                if (lastChar === "'") {
                    if (chars[0] && chars[0] === "'") {
                        lastChar = chars.shift();
                        result += lastChar;
                    }
                    else {
                        break;
                    }
                }
            }
            if (lastChar !== "'") {
                return {
                    type: "UNKNOWN",
                    value: result,
                };
            }
        }
        while (chars[0] && (SYMBOL_CHARS.has(chars[0]) || chars[0].match(unicodeSymbolCharRegexp))) {
            result += chars.shift();
        }
        if (result.length) {
            const value = result;
            const isFunction = value.toUpperCase() in functions$3;
            if (isFunction) {
                return { type: "FUNCTION", value };
            }
            const isReference = value.match(rangeReference);
            if (isReference) {
                return { type: "REFERENCE", value };
            }
            else {
                return { type: "SYMBOL", value };
            }
        }
        return null;
    }
    const whiteSpaceRegexp = /\s/;
    function tokenizeSpace(chars) {
        let spaces = "";
        while (chars[0] && chars[0].match(whiteSpaceRegexp)) {
            spaces += chars.shift();
        }
        if (spaces) {
            return { type: "SPACE", value: spaces };
        }
        return null;
    }
    function tokenizeInvalidRange(chars) {
        if (startsWith(chars, INCORRECT_RANGE_STRING)) {
            chars.splice(0, INCORRECT_RANGE_STRING.length);
            return { type: "INVALID_REFERENCE", value: INCORRECT_RANGE_STRING };
        }
        return null;
    }

    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) {
        var _a;
        let head = 0;
        let transitions = machine[State.LeftRef];
        let matchedTokens = "";
        while (transitions !== undefined) {
            const token = tokens[head++];
            if (!token) {
                return null;
            }
            const transition = (_a = transitions[token.type]) === null || _a === void 0 ? void 0 : _a.find((transition) => transition.guard(token));
            const nextState = transition ? transition.goTo : undefined;
            switch (nextState) {
                case undefined:
                    return null;
                case State.Found:
                    matchedTokens += token.value;
                    tokens.splice(0, head);
                    return {
                        type: "REFERENCE",
                        value: matchedTokens,
                    };
                default:
                    transitions = machine[nextState];
                    matchedTokens += token.value;
                    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) {
        const tokens = tokenize(formula);
        const result = [];
        while (tokens.length) {
            result.push(matchReference(tokens) || tokens.shift());
        }
        return result;
    }

    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,
    };
    const FUNCTION_BP = 6;
    function bindingPower(token) {
        switch (token.type) {
            case "NUMBER":
            case "SYMBOL":
            case "REFERENCE":
                return 0;
            case "COMMA":
                return 3;
            case "LEFT_PAREN":
                return 5;
            case "RIGHT_PAREN":
                return 5;
            case "OPERATOR":
                return OP_PRIORITY[token.value] || 15;
        }
        throw new Error(_lt("Unknown token: %s", token.value));
    }
    function parsePrefix(current, tokens) {
        var _a, _b, _c, _d;
        switch (current.type) {
            case "DEBUGGER":
                const next = parseExpression(tokens, 1000);
                next.debug = true;
                return next;
            case "NUMBER":
                return { type: "NUMBER", value: parseNumber(current.value) };
            case "STRING":
                return { type: "STRING", value: removeStringQuotes(current.value) };
            case "FUNCTION":
                if (tokens.shift().type !== "LEFT_PAREN") {
                    throw new Error(_lt("Wrong function call"));
                }
                else {
                    const args = [];
                    if (tokens[0] && tokens[0].type !== "RIGHT_PAREN") {
                        if (tokens[0].type === "COMMA") {
                            args.push({ type: "UNKNOWN", value: "" });
                        }
                        else {
                            args.push(parseExpression(tokens, FUNCTION_BP));
                        }
                        while (((_a = tokens[0]) === null || _a === void 0 ? void 0 : _a.type) === "COMMA") {
                            tokens.shift();
                            const token = tokens[0];
                            if ((token === null || token === void 0 ? void 0 : token.type) === "RIGHT_PAREN") {
                                args.push({ type: "UNKNOWN", value: "" });
                                break;
                            }
                            else if ((token === null || token === void 0 ? void 0 : token.type) === "COMMA") {
                                args.push({ type: "UNKNOWN", value: "" });
                            }
                            else {
                                args.push(parseExpression(tokens, FUNCTION_BP));
                            }
                        }
                    }
                    const closingToken = tokens.shift();
                    if (!closingToken || closingToken.type !== "RIGHT_PAREN") {
                        throw new Error(_lt("Wrong function call"));
                    }
                    return { type: "FUNCALL", value: current.value, args };
                }
            case "INVALID_REFERENCE":
                return {
                    type: "REFERENCE",
                    value: CellErrorType.InvalidReference,
                };
            case "REFERENCE":
                if (((_b = tokens[0]) === null || _b === void 0 ? void 0 : _b.value) === ":" && ((_c = tokens[1]) === null || _c === void 0 ? void 0 : _c.type) === "REFERENCE") {
                    tokens.shift();
                    const rightReference = tokens.shift();
                    return {
                        type: "REFERENCE",
                        value: `${current.value}:${rightReference === null || rightReference === void 0 ? void 0 : rightReference.value}`,
                    };
                }
                return {
                    type: "REFERENCE",
                    value: current.value,
                };
            case "SYMBOL":
                if (["TRUE", "FALSE"].includes(current.value.toUpperCase())) {
                    return { type: "BOOLEAN", value: current.value.toUpperCase() === "TRUE" };
                }
                else {
                    if (current.value) {
                        if (functionRegex.test(current.value) && ((_d = tokens[0]) === null || _d === void 0 ? void 0 : _d.type) === "LEFT_PAREN") {
                            throw new UnknownFunctionError(current.value);
                        }
                        throw new Error(_lt("Invalid formula"));
                    }
                    return { type: "STRING", value: current.value };
                }
            case "LEFT_PAREN":
                const result = parseExpression(tokens, 5);
                if (!tokens.length || tokens[0].type !== "RIGHT_PAREN") {
                    throw new Error(_lt("Unmatched left parenthesis"));
                }
                tokens.shift();
                return result;
            default:
                if (current.type === "OPERATOR" && UNARY_OPERATORS_PREFIX.includes(current.value)) {
                    return {
                        type: "UNARY_OPERATION",
                        value: current.value,
                        operand: parseExpression(tokens, OP_PRIORITY[current.value]),
                    };
                }
                throw new Error(_lt("Unexpected token: %s", current.value));
        }
    }
    function parseInfix(left, current, tokens) {
        if (current.type === "OPERATOR") {
            const bp = bindingPower(current);
            if (UNARY_OPERATORS_POSTFIX.includes(current.value)) {
                return {
                    type: "UNARY_OPERATION",
                    value: current.value,
                    operand: left,
                    postfix: true,
                };
            }
            else {
                const right = parseExpression(tokens, bp);
                return {
                    type: "BIN_OPERATION",
                    value: current.value,
                    left,
                    right,
                };
            }
        }
        throw new Error(DEFAULT_ERROR_MESSAGE);
    }
    function parseExpression(tokens, bp) {
        const token = tokens.shift();
        if (!token) {
            throw new Error(DEFAULT_ERROR_MESSAGE);
        }
        let expr = parsePrefix(token, tokens);
        while (tokens[0] && bindingPower(tokens[0]) > bp) {
            expr = parseInfix(expr, tokens.shift(), tokens);
        }
        return expr;
    }
    /**
     * Parse an expression (as a string) into an AST.
     */
    function parse(str) {
        return parseTokens(rangeTokenize(str));
    }
    function parseTokens(tokens) {
        tokens = tokens.filter((x) => x.type !== "SPACE");
        if (tokens[0] && tokens[0].type === "OPERATOR" && tokens[0].value === "=") {
            tokens.splice(0, 1);
        }
        const result = parseExpression(tokens, 0);
        if (tokens.length) {
            throw new Error(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.
     *
     * e.g.
     * ```ts
     * convertAstNodes(ast, "FUNCALL", convertFormulaToExcel)
     *
     * function convertFormulaToExcel(ast: ASTFuncall) {
     *   // ...
     *   return modifiedAst
     * }
     * ```
     */
    function convertAstNodes(ast, type, fn) {
        if (type === ast.type) {
            ast = fn(ast);
        }
        switch (ast.type) {
            case "FUNCALL":
                return {
                    ...ast,
                    args: ast.args.map((child) => convertAstNodes(child, type, fn)),
                };
            case "UNARY_OPERATION":
                return {
                    ...ast,
                    operand: convertAstNodes(ast.operand, type, fn),
                };
            case "BIN_OPERATION":
                return {
                    ...ast,
                    right: convertAstNodes(ast.right, type, fn),
                    left: convertAstNodes(ast.left, type, 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$2 = 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",
    };
    /**
     * Takes a list of strings that might be single or multiline
     * and maps them in a list of single line strings.
     */
    function splitCodeLines(codeBlocks) {
        return codeBlocks
            .join("\n")
            .split("\n")
            .filter((line) => line.trim() !== "");
    }
    // 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);
        const { dependencies, constantValues } = formulaArguments(tokens);
        const cacheKey = compilationCacheKey(tokens, dependencies, constantValues);
        if (!functionCache[cacheKey]) {
            const ast = parseTokens([...tokens]);
            let nextId = 1;
            if (ast.type === "BIN_OPERATION" && ast.value === ":") {
                throw new Error(_lt("Invalid formula"));
            }
            if (ast.type === "UNKNOWN") {
                throw new Error(_lt("Invalid formula"));
            }
            const compiledAST = compileAST(ast);
            const code = splitCodeLines([
                `// ${cacheKey}`,
                compiledAST.code,
                `return ${compiledAST.id};`,
            ]).join("\n");
            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);
            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 functionDefinition = functions$2[ast.value.toUpperCase()];
                const currentFunctionArguments = ast.args;
                // check if arguments are supplied in the correct quantities
                const nbrArg = currentFunctionArguments.length;
                if (nbrArg < functionDefinition.minArgRequired) {
                    throw new Error(_lt("Invalid number of arguments for the %s function. Expected %s minimum, but got %s instead.", ast.value.toUpperCase(), functionDefinition.minArgRequired.toString(), nbrArg.toString()));
                }
                if (nbrArg > functionDefinition.maxArgPossible) {
                    throw new Error(_lt("Invalid number of arguments for the %s function. Expected %s maximum, but got %s instead.", ast.value.toUpperCase(), functionDefinition.maxArgPossible.toString(), nbrArg.toString()));
                }
                const repeatingArg = functionDefinition.nbrArgRepeating;
                if (repeatingArg > 1) {
                    const argBeforeRepeat = functionDefinition.args.length - repeatingArg;
                    const nbrRepeatingArg = nbrArg - argBeforeRepeat;
                    if (nbrRepeatingArg % repeatingArg !== 0) {
                        throw new Error(_lt("Invalid number of arguments for the %s function. Expected all arguments after position %s to be supplied by groups of %s arguments", ast.value.toUpperCase(), argBeforeRepeat.toString(), repeatingArg.toString()));
                    }
                }
                let listArgs = [];
                for (let i = 0; i < nbrArg; i++) {
                    const argPosition = functionDefinition.getArgToFocus(i + 1) - 1;
                    if (0 <= argPosition && argPosition < functionDefinition.args.length) {
                        const currentArg = currentFunctionArguments[i];
                        const argDefinition = functionDefinition.args[argPosition];
                        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) => t === "RANGE" ||
                            t === "RANGE<BOOLEAN>" ||
                            t === "RANGE<DATE>" ||
                            t === "RANGE<NUMBER>" ||
                            t === "RANGE<STRING>");
                        const isRangeOnly = argTypes.every((t) => t === "RANGE" ||
                            t === "RANGE<BOOLEAN>" ||
                            t === "RANGE<DATE>" ||
                            t === "RANGE<NUMBER>" ||
                            t === "RANGE<STRING>");
                        if (isRangeOnly) {
                            if (currentArg.type !== "REFERENCE") {
                                throw new Error(_lt("Function %s expects the parameter %s to be reference to a cell or range, not a %s.", ast.value.toUpperCase(), (i + 1).toString(), currentArg.type.toLowerCase()));
                            }
                        }
                        const compiledAST = compileAST(currentArg, isLazy, isMeta, hasRange, {
                            functionName: ast.value.toUpperCase(),
                            paramIndex: i + 1,
                        });
                        listArgs.push(compiledAST);
                    }
                }
                return listArgs;
            }
            /**
             * 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 two parameters “isLazy” and “isMeta”. These
             * parameters may vary when compiling function arguments:
             *
             * - isLazy: In some cases the function arguments does not need to be
             * evaluated before entering the functions. For example the IF function might
             * take invalid arguments that do not need to be evaluate and thus should not
             * create an error. For this we have lazy 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, isLazy = false, isMeta = false, hasRange = false, referenceVerification = {}) {
                const codeBlocks = [];
                let id, fnName, statement;
                if (ast.type !== "REFERENCE" && !(ast.type === "BIN_OPERATION" && ast.value === ":")) {
                    if (isMeta) {
                        throw new Error(_lt(`Argument must be a reference to a cell or range.`));
                    }
                }
                if (ast.debug) {
                    codeBlocks.push("debugger;");
                }
                switch (ast.type) {
                    case "BOOLEAN":
                        if (!isLazy) {
                            return { id: `{ value: ${ast.value} }`, code: "" };
                        }
                        id = nextId++;
                        statement = `{ value: ${ast.value} }`;
                        break;
                    case "NUMBER":
                        id = nextId++;
                        statement = `{ value: this.constantValues.numbers[${constantValues.numbers.indexOf(ast.value)}] }`;
                        break;
                    case "STRING":
                        id = nextId++;
                        statement = `{ value: this.constantValues.strings[${constantValues.strings.indexOf(ast.value)}] }`;
                        break;
                    case "REFERENCE":
                        const referenceIndex = dependencies.indexOf(ast.value);
                        id = nextId++;
                        if (hasRange) {
                            statement = `range(deps[${referenceIndex}])`;
                        }
                        else {
                            statement = `ref(deps[${referenceIndex}], ${isMeta ? "true" : "false"}, "${referenceVerification.functionName || OPERATOR_MAP["="]}",  ${referenceVerification.paramIndex})`;
                        }
                        break;
                    case "FUNCALL":
                        id = nextId++;
                        const args = compileFunctionArgs(ast);
                        codeBlocks.push(args.map((arg) => arg.code).join("\n"));
                        fnName = ast.value.toUpperCase();
                        codeBlocks.push(`ctx.__lastFnCalled = '${fnName}';`);
                        statement = `ctx['${fnName}'](${args.map((arg) => arg.id)})`;
                        break;
                    case "UNARY_OPERATION": {
                        id = nextId++;
                        fnName = UNARY_OPERATOR_MAP[ast.value];
                        const operand = compileAST(ast.operand, false, false, false, {
                            functionName: fnName,
                        });
                        codeBlocks.push(operand.code);
                        codeBlocks.push(`ctx.__lastFnCalled = '${fnName}';`);
                        statement = `ctx['${fnName}'](${operand.id})`;
                        break;
                    }
                    case "BIN_OPERATION": {
                        id = nextId++;
                        fnName = OPERATOR_MAP[ast.value];
                        const left = compileAST(ast.left, false, false, false, {
                            functionName: fnName,
                        });
                        const right = compileAST(ast.right, false, false, false, {
                            functionName: fnName,
                        });
                        codeBlocks.push(left.code);
                        codeBlocks.push(right.code);
                        codeBlocks.push(`ctx.__lastFnCalled = '${fnName}';`);
                        statement = `ctx['${fnName}'](${left.id}, ${right.id})`;
                        break;
                    }
                    case "UNKNOWN":
                        if (!isLazy) {
                            return { id: "undefined", code: "" };
                        }
                        id = nextId++;
                        statement = `undefined`;
                        break;
                }
                if (isLazy) {
                    const lazyFunction = `const _${id} = () => {\n` +
                        `\t${splitCodeLines(codeBlocks).join("\n\t")}\n` +
                        `\treturn ${statement};\n` +
                        "}";
                    return { id: `_${id}`, code: lazyFunction };
                }
                else {
                    codeBlocks.push(`let _${id} = ${statement};`);
                    return { id: `_${id}`, code: codeBlocks.join("\n") };
                }
            }
        }
        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))}|`;
                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);
                    if (!constantValues.numbers.includes(value)) {
                        constantValues.numbers.push(value);
                    }
                    break;
                }
            }
        }
        return {
            dependencies,
            constantValues,
        };
    }

    /**
     * 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 "FUNCTION":
                    functionStarted = token.value;
                    break;
                case "LEFT_PAREN":
                    stack.push({ parent: functionStarted, argPosition: 0 });
                    functionStarted = "";
                    break;
                case "RIGHT_PAREN":
                    stack.pop();
                    break;
                case "COMMA":
                    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) {
        const tokens = rangeTokenize(formula);
        return mapParentFunction(mapParenthesis(enrichTokens(tokens)));
    }

    /**
     * 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 ? `${getComposerSheetName(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("$");
    }

    /**
     * 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 {
        constructor(stateObserver, dispatch, config) {
            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 0 /* 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 !== 0 /* CommandResult.Success */);
                    if (cancelledReasons.length) {
                        return cancelledReasons;
                    }
                }
                return 0 /* CommandResult.Success */;
            };
        }
        checkValidations(command, ...validations) {
            return this.batchValidations(...validations)(command);
        }
    }
    BasePlugin.getters = [];

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

    const CELL_DELETED_MESSAGE = _lt("The cell you are trying to edit has been deleted.");
    const SelectionIndicator = "␣";
    class EditionPlugin extends UIPlugin {
        constructor() {
            super(...arguments);
            this.col = 0;
            this.row = 0;
            this.mode = "inactive";
            this.sheetId = "";
            this.currentContent = "";
            this.currentTokens = [];
            this.selectionStart = 0;
            this.selectionEnd = 0;
            this.selectionInitialStart = 0;
            this.initialContent = "";
            this.previousRef = "";
            this.previousRange = undefined;
            this.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);
                    }
                    else {
                        return 0 /* CommandResult.Success */;
                    }
                case "START_EDITION":
                    if (cmd.selection) {
                        const cell = this.getters.getActiveCell();
                        const content = cmd.text || (cell === null || cell === void 0 ? void 0 : cell.composerContent) || "";
                        return this.validateSelection(content.length, cmd.selection.start, cmd.selection.end);
                    }
                    else {
                        return 0 /* CommandResult.Success */;
                    }
                default:
                    return 0 /* CommandResult.Success */;
            }
        }
        handleEvent(event) {
            if (this.mode !== "selecting") {
                return;
            }
            switch (event.mode) {
                case "newAnchor":
                    this.insertSelectedRange(event.anchor.zone);
                    break;
                default:
                    this.replaceSelectedRanges(event.anchor.zone);
                    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":
                    this.startEdition(cmd.text, cmd.selection);
                    this.updateRangeColor();
                    break;
                case "STOP_EDITION":
                    if (cmd.cancel) {
                        this.resetContent();
                        this.cancelEditionAndActivateSheet();
                    }
                    else {
                        this.stopEdition();
                    }
                    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.resetContent();
                    this.cancelEditionAndActivateSheet();
                    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":
                    this.dispatch("STOP_COMPOSER_RANGE_SELECTION");
                    const range = this.getters.getRangeFromRangeData(cmd.range);
                    const previousRefToken = this.currentTokens
                        .filter((token) => token.type === "REFERENCE")
                        .find((token) => {
                        const { xc, sheetName: sheet } = splitReference(token.value);
                        const sheetName = sheet || this.getters.getSheetName(this.sheetId);
                        const activeSheetId = this.getters.getActiveSheetId();
                        if (!isSheetNameEqual(this.getters.getSheetName(activeSheetId), sheetName)) {
                            return false;
                        }
                        const refRange = this.getters.getRangeFromSheetXC(activeSheetId, xc);
                        return isEqual(this.getters.expandZone(activeSheetId, refRange.zone), range.zone);
                    });
                    this.previousRef = previousRefToken.value;
                    this.previousRange = this.getters.getRangeFromSheetXC(this.getters.getActiveSheetId(), this.previousRef);
                    this.selectionInitialStart = previousRefToken.start;
                    break;
                case "CHANGE_HIGHLIGHT":
                    const cmdRange = this.getters.getRangeFromRangeData(cmd.range);
                    const newRef = this.getRangeReference(cmdRange, this.previousRange.parts);
                    this.selectionStart = this.selectionInitialStart;
                    this.selectionEnd = this.selectionInitialStart + this.previousRef.length;
                    this.replaceSelection(newRef);
                    this.previousRef = newRef;
                    this.selectionStart = this.currentContent.length;
                    this.selectionEnd = this.currentContent.length;
                    break;
                case "ACTIVATE_SHEET":
                    if (!this.currentContent.startsWith("=")) {
                        this.cancelEdition();
                        this.resetContent();
                    }
                    if (cmd.sheetIdFrom !== cmd.sheetIdTo) {
                        const { col, row } = this.getters.getNextVisibleCellPosition(cmd.sheetIdTo, 0, 0);
                        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.resetContent();
                        this.cancelEditionAndActivateSheet();
                        this.ui.notifyUI({
                            type: "ERROR",
                            text: CELL_DELETED_MESSAGE,
                        });
                    }
                    break;
                case "CYCLE_EDITION_REFERENCES":
                    this.cycleReferences();
                    break;
            }
        }
        // ---------------------------------------------------------------------------
        // Getters
        // ---------------------------------------------------------------------------
        getEditionMode() {
            return this.mode;
        }
        getCurrentContent() {
            if (this.mode === "inactive") {
                const cell = this.getters.getActiveCell();
                return (cell === null || cell === void 0 ? void 0 : cell.composerContent) || "";
            }
            return this.currentContent;
        }
        getEditionSheet() {
            return this.sheetId;
        }
        getComposerSelection() {
            return {
                start: this.selectionStart,
                end: this.selectionEnd,
            };
        }
        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);
            }
        }
        // ---------------------------------------------------------------------------
        // Misc
        // ---------------------------------------------------------------------------
        cycleReferences() {
            const tokens = this.getTokensInSelection();
            const refTokens = tokens.filter((token) => token.type === "REFERENCE");
            if (refTokens.length === 0)
                return;
            const updatedReferences = tokens
                .map(loopThroughReferenceType)
                .map((token) => token.value)
                .join("");
            const content = this.currentContent;
            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 selection = { start: startOfTokens, end: endOfTokens };
            // Put the selection at the end of the token if we cycled on a single token
            if (refTokens.length === 1 && this.selectionStart === this.selectionEnd) {
                selection.start = selection.end;
            }
            this.dispatch("SET_CURRENT_CONTENT", {
                content: newContent,
                selection,
            });
        }
        validateSelection(length, start, end) {
            return start >= 0 && start <= length && end >= 0 && end <= length
                ? 0 /* CommandResult.Success */
                : 47 /* CommandResult.WrongComposerSelection */;
        }
        onColumnsRemoved(cmd) {
            if (cmd.elements.includes(this.col) && this.mode !== "inactive") {
                this.dispatch("STOP_EDITION", { cancel: true });
                this.ui.notifyUI({
                    type: "ERROR",
                    text: 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("STOP_EDITION", { cancel: true });
                this.ui.notifyUI({
                    type: "ERROR",
                    text: 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";
            this.selectionInitialStart = this.selectionStart;
        }
        /**
         * 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) {
            var _a;
            const cell = this.getters.getActiveCell();
            if (str && ((_a = cell === null || cell === void 0 ? void 0 : cell.format) === null || _a === void 0 ? void 0 : _a.includes("%")) && isNumber(str)) {
                selection = selection || { start: str.length, end: str.length };
                str = `${str}%`;
            }
            this.initialContent = (cell === null || cell === void 0 ? void 0 : cell.composerContent) || "";
            this.mode = "editing";
            const { col, row } = this.getters.getPosition();
            this.col = col;
            this.row = row;
            this.sheetId = this.getters.getActiveSheetId();
            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") {
                const activeSheetId = this.getters.getActiveSheetId();
                this.cancelEditionAndActivateSheet();
                const { col, row } = this.getters.getMainCellPosition(this.sheetId, this.col, this.row);
                let content = this.currentContent;
                const didChange = this.initialContent !== content;
                if (!didChange) {
                    return;
                }
                if (content) {
                    const cell = this.getters.getCell(activeSheetId, col, 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 === null || cell === void 0 ? void 0 : cell.isLink()) {
                        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("");
            }
        }
        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,
                });
            }
        }
        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) {
            text = text.replace(/[\r\n]/g, "");
            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") {
                this.currentTokens = text.startsWith("=") ? composerTokenize(text) : [];
                if (this.currentTokens.length > 100) {
                    if (raise) {
                        this.ui.notifyUI({
                            type: "ERROR",
                            text: _lt("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);
                this.selectionInitialStart = start;
            }
            else {
                this.insertText("," + ref, start);
                this.selectionInitialStart = start + 1;
            }
        }
        /**
         * Replace the current reference selected by the new one.
         * */
        replaceSelectedRanges(zone) {
            const ref = this.getZoneReference(zone);
            this.replaceText(ref, this.selectionInitialStart, this.selectionEnd);
        }
        getZoneReference(zone, fixedParts = [{ colFixed: false, rowFixed: false }]) {
            const sheetId = this.getters.getActiveSheetId();
            let selectedXc = this.getters.zoneToXC(sheetId, zone, fixedParts);
            if (this.getters.getEditionSheet() !== this.getters.getActiveSheetId()) {
                const sheetName = getComposerSheetName(this.getters.getSheetName(this.getters.getActiveSheetId()));
                selectedXc = `${sheetName}!${selectedXc}`;
            }
            return selectedXc;
        }
        getRangeReference(range, fixedParts = [{ colFixed: false, rowFixed: false }]) {
            if (fixedParts.length === 1 && getZoneArea(range.zone) > 1) {
                fixedParts.push({ ...fixedParts[0] });
            }
            else if (fixedParts.length === 2 && getZoneArea(range.zone) === 1) {
                fixedParts.pop();
            }
            const newRange = range.clone({ parts: this.previousRange.parts });
            return this.getters.getSelectionRangeString(newRange, this.getters.getEditionSheet());
        }
        /**
         * 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.getEditionSheet();
            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.getEditionSheet();
            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.getEditionSheet();
            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);
        }
        /**
         * Function used to determine when composer selection can start.
         * Three conditions are necessary:
         * - the previous token is among ["COMMA", "LEFT_PAREN", "OPERATOR"], and is not a postfix unary operator
         * - the next token is missing or is among ["COMMA", "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 (!["COMMA", "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 && !["COMMA", "RIGHT_PAREN", "OPERATOR"].includes(currentToken.type)) {
                    if (currentToken.type !== "SPACE") {
                        return false;
                    }
                    count++;
                    currentToken = this.currentTokens[count];
                }
                return true;
            }
            return false;
        }
        /**
         * Return all the tokens between selectionStart and selectionEnd.
         * Includes token that begin right on selectionStart or end right on selectionEnd.
         */
        getTokensInSelection() {
            const start = Math.min(this.selectionStart, this.selectionEnd);
            const end = Math.max(this.selectionStart, this.selectionEnd);
            return this.currentTokens.filter((t) => (t.start <= start && t.end >= start) || (t.start >= start && t.start < end));
        }
    }
    EditionPlugin.getters = [
        "getEditionMode",
        "isSelectingForComposer",
        "showSelectionIndicator",
        "getCurrentContent",
        "getEditionSheet",
        "getComposerSelection",
        "getCurrentTokens",
        "getTokenAtCursor",
        "getComposerHighlights",
    ];

    const functions$1 = functionRegistry.content;
    const providerRegistry = new Registry();
    providerRegistry.add("functions", () => {
        return Object.keys(functions$1).map((key) => {
            return {
                text: key,
                description: functions$1[key].description,
            };
        });
    });
    // -----------------------------------------------------------------------------
    // Autocomplete DropDown component
    // -----------------------------------------------------------------------------
    css /* scss */ `
  .o-autocomplete-dropdown {
    pointer-events: auto;
    background-color: #fff;
    & > div:hover {
      background-color: #f2f2f2;
    }
    .o-autocomplete-value-focus {
      background-color: rgba(0, 0, 0, 0.08);
    }

    & > div {
      display: flex;
      flex-direction: column;
      padding: 1px 0 5px 5px;
      .o-autocomplete-description {
        padding: 0 0 0 5px;
        font-size: 11px;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
      }
    }
  }
`;
    class TextValueProvider extends owl.Component {
        constructor() {
            super(...arguments);
            this.state = owl.useState({
                values: [],
                selectedIndex: 0,
            });
        }
        setup() {
            owl.onMounted(() => this.filter(this.props.search));
            owl.onWillUpdateProps((nextProps) => this.checkUpdateProps(nextProps));
            this.props.exposeAPI({
                getValueToFill: () => this.getValueToFill(),
                moveDown: () => this.moveDown(),
                moveUp: () => this.moveUp(),
            });
        }
        checkUpdateProps(nextProps) {
            if (nextProps.search !== this.props.search) {
                this.filter(nextProps.search);
            }
        }
        async filter(searchTerm) {
            const provider = providerRegistry.get(this.props.provider);
            let values = provider();
            if (this.props.filter) {
                values = this.props.filter(searchTerm, values);
            }
            else {
                values = values
                    .filter((t) => t.text.toUpperCase().startsWith(searchTerm.toUpperCase()))
                    .sort((l, r) => (l.text < r.text ? -1 : l.text > r.text ? 1 : 0));
            }
            this.state.values = values.slice(0, 10);
            this.state.selectedIndex = 0;
        }
        fillValue(index) {
            this.state.selectedIndex = index;
            this.props.onCompleted(this.getValueToFill());
        }
        moveDown() {
            this.state.selectedIndex = (this.state.selectedIndex + 1) % this.state.values.length;
        }
        moveUp() {
            this.state.selectedIndex--;
            if (this.state.selectedIndex < 0) {
                this.state.selectedIndex = this.state.values.length - 1;
            }
        }
        getValueToFill() {
            if (this.state.values.length) {
                return this.state.values[this.state.selectedIndex].text;
            }
            return undefined;
        }
    }
    TextValueProvider.template = "o-spreadsheet-TextValueProvider";

    class ContentEditableHelper {
        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();
            this.removeSelection();
            let range = document.createRange();
            if (start == end && start === 0) {
                range.setStart(this.el, 0);
                range.setEnd(this.el, 0);
                selection.addRange(range);
            }
            else {
                if (start < 0 || end > this.el.textContent.length) {
                    console.warn(`wrong selection asked start ${start}, end ${end}, text content length ${this.el.textContent.length}`);
                    if (start < 0)
                        start = 0;
                    if (end > this.el.textContent.length)
                        end = this.el.textContent.length;
                    if (start > this.el.textContent.length)
                        start = this.el.textContent.length;
                }
                let startNode = this.findChildAtCharacterIndex(start);
                let endNode = this.findChildAtCharacterIndex(end);
                range.setStart(startNode.node, startNode.offset);
                selection.addRange(range);
                selection.extend(endNode.node, endNode.offset);
            }
        }
        /**
         * finds the dom element that contains the character at `offset`
         */
        findChildAtCharacterIndex(offset) {
            let it = this.iterateChildren(this.el);
            let current, previous;
            let usedCharacters = offset;
            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 {
                        it.return(current.value);
                    }
                    previous = current.value;
                }
            } while (!current.done);
            if (current.value) {
                return { node: current.value, offset: usedCharacters };
            }
            return { node: previous, offset: usedCharacters };
        }
        /**
         * Iterate over the dom tree starting at `el` and over all the children depth first.
         * */
        *iterateChildren(el) {
            yield el;
            if (el.hasChildNodes()) {
                for (let child of el.childNodes) {
                    yield* this.iterateChildren(child);
                }
            }
        }
        /**
         * Sets (or Replaces all) the text inside the root element in the form of distinctive
         * span for each element provided in `contents`.
         *
         * Each span will have its own fontcolor and specific class if provided in the HtmlContent object.
         */
        setText(contents) {
            if (contents.length === 0) {
                return;
            }
            for (const content of contents) {
                const span = document.createElement("span");
                span.innerText = content.value;
                if (content.color) {
                    span.style.color = content.color;
                }
                if (content.class) {
                    span.classList.add(content.class);
                }
                this.el.appendChild(span);
            }
        }
        /**
         * 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 it = this.iterateChildren(this.el);
            let usedCharacters = 0;
            let current = it.next();
            while (!current.done && current.value !== nodeToFind) {
                if (!current.value.hasChildNodes()) {
                    if (current.value.textContent) {
                        usedCharacters += current.value.textContent.length;
                    }
                }
                current = it.next();
            }
            if (current.value === nodeToFind) {
                if (!current.value.hasChildNodes()) {
                    usedCharacters += nodeOffset;
                }
                else {
                    const children = [...current.value.childNodes].slice(0, nodeOffset);
                    for (const child of children) {
                        if (!child.textContent)
                            continue;
                        usedCharacters += child.textContent.length;
                    }
                }
            }
            return usedCharacters;
        }
        getStartAndEndSelection() {
            const selection = document.getSelection();
            return {
                startElement: selection.anchorNode || this.el,
                startSelectionOffset: selection.anchorOffset,
                endElement: selection.focusNode || this.el,
                endSelectionOffset: selection.focusOffset,
            };
        }
    }

    // -----------------------------------------------------------------------------
    // Formula Assistant component
    // -----------------------------------------------------------------------------
    css /* scss */ `
  .o-formula-assistant {
    white-space: normal;
    background-color: #fff;
    .o-formula-assistant-head {
      background-color: #f2f2f2;
      padding: 10px;
    }
    .o-formula-assistant-core {
      padding: 0px 0px 10px 0px;
      margin: 10px;
      border-bottom: 1px solid gray;
    }
    .o-formula-assistant-arg {
      padding: 0px 10px 10px 10px;
      display: flex;
      flex-direction: column;
    }
    .o-formula-assistant-arg-description {
      font-size: 85%;
    }
    .o-formula-assistant-focus {
      div:first-child,
      span {
        color: purple;
        text-shadow: 0px 0px 1px purple;
      }
      div:last-child {
        color: black;
      }
    }
    .o-formula-assistant-gray {
      color: gray;
    }
  }
  .o-formula-assistant-container {
    user-select: none;
  }
  .o-formula-assistant-event-none {
    pointer-events: none;
  }
  .o-formula-assistant-event-auto {
    pointer-events: auto;
  }
  .o-formula-assistant-transparency {
    opacity: 0.3;
  }
`;
    class FunctionDescriptionProvider extends owl.Component {
        constructor() {
            super(...arguments);
            this.assistantState = owl.useState({
                allowCellSelectionBehind: false,
            });
            this.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.template = "o-spreadsheet-FunctionDescriptionProvider";

    const functions = functionRegistry.content;
    const ASSISTANT_WIDTH = 300;
    const FunctionColor = "#4a4e4d";
    const OperatorColor = "#3da4ab";
    const StringColor = "#00a82d";
    const SelectionIndicatorColor = "darkgrey";
    const NumberColor = "#02c39a";
    const MatchingParenColor = "black";
    const SelectionIndicatorClass = "selector-flag";
    const tokenColor = {
        OPERATOR: OperatorColor,
        NUMBER: NumberColor,
        STRING: StringColor,
        FUNCTION: FunctionColor,
        DEBUGGER: OperatorColor,
        LEFT_PAREN: FunctionColor,
        RIGHT_PAREN: FunctionColor,
        COMMA: FunctionColor,
    };
    css /* scss */ `
  .o-composer-container {
    padding: 0;
    margin: 0;
    border: 0;
    flex-grow: 1;
    max-height: inherit;
    .o-composer {
      font-family: ${DEFAULT_FONT};
      caret-color: black;
      padding-left: 3px;
      padding-right: 3px;
      word-break: break-all;
      &:focus {
        outline: none;
      }
      &.unfocusable {
        pointer-events: none;
      }
      span {
        white-space: pre;
        &.${SelectionIndicatorClass}:after {
          content: "${SelectionIndicator}";
          color: ${SelectionIndicatorColor};
        }
      }
    }
    .o-composer-assistant {
      position: absolute;
      margin: 4px;
      pointer-events: none;
    }
  }

  /* Custom css to highlight topbar composer on focus */
  .o-topbar-toolbar .o-composer-container[active] {
    border: 1px solid ${SELECTION_BORDER_COLOR} !important;
  }

  .o-topbar-toolbar .o-composer-container[active] {
    z-index: ${ComponentsImportance.TopBarComposer};
  }
`;
    class Composer extends owl.Component {
        constructor() {
            super(...arguments);
            this.composerRef = owl.useRef("o_composer");
            this.spreadsheetRect = useSpreadsheetRect();
            this.contentHelper = new ContentEditableHelper(this.composerRef.el);
            this.composerState = owl.useState({
                positionStart: 0,
                positionEnd: 0,
            });
            this.autoCompleteState = owl.useState({
                showProvider: false,
                provider: "functions",
                search: "",
            });
            this.functionDescriptionState = owl.useState({
                showDescription: false,
                functionName: "",
                functionDescription: {},
                argToFocus: 0,
            });
            this.isKeyStillDown = false;
            this.compositionActive = false;
            this.borderStyle = `box-shadow: 0 1px 4px 3px rgba(60, 64, 67, 0.15);`;
            // we can't allow input events to be triggered while we remove and add back the content of the composer in processContent
            this.shouldProcessInputEvents = false;
            this.tokens = [];
            this.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),
            };
        }
        get assistantStyle() {
            const composerRect = this.composerRef.el.getBoundingClientRect();
            const 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);
                if (cellY > remainingHeight) {
                    // render top
                    assistantStyle.top = "-8px";
                    assistantStyle.transform = "translate(0, -100%)";
                }
                if (cellX + ASSISTANT_WIDTH > this.props.delimitation.width) {
                    // render left
                    assistantStyle.right = "0px";
                }
                return cssPropertiesToCss(assistantStyle);
            }
            if (composerRect.left + ASSISTANT_WIDTH > this.spreadsheetRect.width) {
                assistantStyle.right = "0px";
            }
            return cssPropertiesToCss(assistantStyle);
        }
        setup() {
            owl.onMounted(() => {
                const el = this.composerRef.el;
                if (this.props.isDefaultFocus) {
                    this.env.focusableElement.setFocusableElement(el);
                }
                this.contentHelper.updateEl(el);
                this.processContent();
            });
            owl.onPatched(() => {
                if (!this.isKeyStillDown) {
                    this.processContent();
                }
                // Required because typing '=SUM' and double-clicking another cell leaves ShowProvider and ShowDescription true
                if (this.env.model.getters.getEditionMode() === "inactive") {
                    this.processTokenAtCursor();
                }
            });
        }
        // ---------------------------------------------------------------------------
        // 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
                // but don't stop the event and let it bubble to the grid which will
                // update the selection accordingly
                ev.preventDefault();
                return;
            }
            const content = this.env.model.getters.getCurrentContent();
            if (this.props.focus === "cellFocus" &&
                !this.autoCompleteState.showProvider &&
                !content.startsWith("=")) {
                this.env.model.dispatch("STOP_EDITION");
                return;
            }
            ev.stopPropagation();
            // only for arrow up and down
            if (["ArrowUp", "ArrowDown"].includes(ev.key) &&
                this.autoCompleteState.showProvider &&
                this.autocompleteAPI) {
                ev.preventDefault();
                if (ev.key === "ArrowUp") {
                    this.autocompleteAPI.moveUp();
                }
                else {
                    this.autocompleteAPI.moveDown();
                }
            }
            this.updateCursorIfNeeded();
        }
        processTabKey(ev) {
            ev.preventDefault();
            ev.stopPropagation();
            if (this.env.model.getters.getEditionMode() !== "inactive") {
                if (this.autoCompleteState.showProvider && this.autocompleteAPI) {
                    const autoCompleteValue = this.autocompleteAPI.getValueToFill();
                    if (autoCompleteValue) {
                        this.autoComplete(autoCompleteValue);
                        return;
                    }
                }
                else {
                    // when completing with tab, if there is no value to complete, the active cell will be moved to the right.
                    // we can't let the model think that it is for a ref selection.
                    // todo: check if this can be removed someday
                    this.env.model.dispatch("STOP_COMPOSER_RANGE_SELECTION");
                }
                this.env.model.dispatch("STOP_EDITION");
            }
            const direction = ev.shiftKey ? "left" : "right";
            this.env.model.selection.moveAnchorCell(direction, 1);
        }
        processEnterKey(ev) {
            ev.preventDefault();
            ev.stopPropagation();
            this.isKeyStillDown = false;
            if (this.autoCompleteState.showProvider && this.autocompleteAPI) {
                const autoCompleteValue = this.autocompleteAPI.getValueToFill();
                if (autoCompleteValue) {
                    this.autoComplete(autoCompleteValue);
                    return;
                }
            }
            this.env.model.dispatch("STOP_EDITION");
            const direction = ev.shiftKey ? "up" : "down";
            this.env.model.selection.moveAnchorCell(direction, 1);
        }
        processEscapeKey() {
            this.env.model.dispatch("STOP_EDITION", { cancel: true });
        }
        processF4Key() {
            this.env.model.dispatch("CYCLE_EDITION_REFERENCES");
            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];
            if (handler) {
                handler.call(this, ev);
            }
            else {
                ev.stopPropagation();
                this.updateCursorIfNeeded();
            }
        }
        updateCursorIfNeeded() {
            const moveCursor = !this.env.model.getters.isSelectingForComposer() &&
                !(this.env.model.getters.getEditionMode() === "inactive");
            if (moveCursor) {
                const { start, end } = this.contentHelper.getCurrentSelection();
                this.env.model.dispatch("CHANGE_COMPOSER_CURSOR_SELECTION", { start, end });
                this.isKeyStillDown = true;
            }
        }
        onPaste(ev) {
            if (this.env.model.getters.getEditionMode() !== "inactive") {
                // let the browser clipboard work
                ev.stopPropagation();
            }
            else {
                // the user meant to paste in the sheet, not open the composer with the pasted content
                // While we're not editing, we still have the focus and should therefore prevent
                // the native "paste" to occur.
                ev.preventDefault();
            }
        }
        /*
         * Triggered automatically by the content-editable between the keydown and key up
         * */
        onInput(ev) {
            var _a, _b;
            if (!this.shouldProcessInputEvents) {
                return;
            }
            ev.stopPropagation();
            let content;
            if (this.env.model.getters.getEditionMode() === "inactive") {
                content = ev.data || "";
            }
            else {
                const el = this.composerRef.el;
                content = el.childNodes.length ? el.textContent : "";
            }
            if (this.props.focus === "inactive") {
                return (_b = (_a = this.props).onComposerCellFocused) === null || _b === void 0 ? void 0 : _b.call(_a, content);
            }
            this.env.model.dispatch("STOP_COMPOSER_RANGE_SELECTION");
            this.env.model.dispatch("SET_CURRENT_CONTENT", {
                content,
                selection: this.contentHelper.getCurrentSelection(),
            });
        }
        onKeyup(ev) {
            this.isKeyStillDown = false;
            if (this.props.focus === "inactive" ||
                ["Control", "Shift", "Tab", "Enter", "F4"].includes(ev.key)) {
                return;
            }
            if (this.autoCompleteState.showProvider && ["ArrowUp", "ArrowDown"].includes(ev.key)) {
                return; // already processed in keydown
            }
            if (this.env.model.getters.isSelectingForComposer() &&
                ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(ev.key)) {
                return; // already processed in keydown
            }
            ev.preventDefault();
            ev.stopPropagation();
            this.autoCompleteState.showProvider = false;
            if (ev.ctrlKey && ev.key === " ") {
                this.autoCompleteState.search = "";
                this.autoCompleteState.showProvider = true;
                this.env.model.dispatch("STOP_COMPOSER_RANGE_SELECTION");
                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", this.contentHelper.getCurrentSelection());
            }
            this.processTokenAtCursor();
            this.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();
        }
        onBlur() {
            this.isKeyStillDown = false;
        }
        onCompleted(text) {
            text && this.autoComplete(text);
        }
        onContextMenu(ev) {
            var _a, _b;
            if (this.env.model.getters.getEditionMode() === "inactive") {
                (_b = (_a = this.props).onInputContextMenu) === null || _b === void 0 ? void 0 : _b.call(_a, ev);
            }
        }
        // ---------------------------------------------------------------------------
        // Private
        // ---------------------------------------------------------------------------
        processContent() {
            if (this.compositionActive) {
                return;
            }
            this.contentHelper.removeAll(); // removes the content of the composer, to be added just after
            this.shouldProcessInputEvents = false;
            if (this.props.focus !== "inactive") {
                this.contentHelper.el.focus();
                this.contentHelper.selectRange(0, 0); // move the cursor inside the composer at 0 0.
            }
            const content = this.getContent();
            if (content.length !== 0) {
                this.contentHelper.setText(content);
                const { start, end } = this.env.model.getters.getComposerSelection();
                if (this.props.focus !== "inactive") {
                    // Put the cursor back where it was before the rendering
                    this.contentHelper.selectRange(start, end);
                }
            }
            this.shouldProcessInputEvents = true;
        }
        getContent() {
            let content;
            const value = this.env.model.getters.getCurrentContent();
            const isValidFormula = value.startsWith("=") && this.env.model.getters.getCurrentTokens().length > 0;
            if (value === "") {
                content = [];
            }
            else if (isValidFormula && this.props.focus !== "inactive") {
                content = this.getColoredTokens();
            }
            else {
                content = [{ value }];
            }
            return content;
        }
        getColoredTokens() {
            const tokens = this.env.model.getters.getCurrentTokens();
            const tokenAtCursor = this.env.model.getters.getTokenAtCursor();
            const result = [];
            const { start, end } = this.env.model.getters.getComposerSelection();
            for (const token of tokens) {
                switch (token.type) {
                    case "OPERATOR":
                    case "NUMBER":
                    case "FUNCTION":
                    case "COMMA":
                    case "STRING":
                        result.push({ value: token.value, color: tokenColor[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":
                        let value = token.value;
                        if (["TRUE", "FALSE"].includes(value.toUpperCase())) {
                            result.push({ value: token.value, color: NumberColor });
                        }
                        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: MatchingParenColor  });
                        }
                        else {
                            result.push({ value: token.value, color: tokenColor[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;
        }
        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.getEditionSheet();
            const highlight = highlights.find((highlight) => {
                if (highlight.sheetId !== refSheet)
                    return false;
                const range = this.env.model.getters.getRangeFromSheetXC(refSheet, xc);
                let zone = range.zone;
                const { height, width } = zoneToDimension(zone);
                zone = height * width === 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;
            if (content.startsWith("=")) {
                const tokenAtCursor = this.env.model.getters.getTokenAtCursor();
                if (tokenAtCursor) {
                    const { xc } = splitReference(tokenAtCursor.value);
                    if (tokenAtCursor.type === "FUNCTION" ||
                        (tokenAtCursor.type === "SYMBOL" && !rangeReference.test(xc))) {
                        // initialize Autocomplete Dropdown
                        this.autoCompleteState.search = tokenAtCursor.value;
                        this.autoCompleteState.showProvider = true;
                    }
                    else if (tokenAtCursor.functionContext && tokenAtCursor.type !== "UNKNOWN") {
                        // initialize Formula Assistant
                        const tokenContext = tokenAtCursor.functionContext;
                        const parentFunction = tokenContext.parent.toUpperCase();
                        const description = functions[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) {
                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 (this.autoCompleteState.provider && 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,
                });
            }
            this.processTokenAtCursor();
        }
    }
    Composer.template = "o-spreadsheet-Composer";
    Composer.components = { TextValueProvider, FunctionDescriptionProvider };
    Composer.defaultProps = {
        inputStyle: "",
        focus: "inactive",
        isDefaultFocus: false,
    };

    const COMPOSER_BORDER_WIDTH = 3 * 0.4 * window.devicePixelRatio || 1;
    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};
  }
`;
    /**
     * 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 {
        constructor() {
            super(...arguments);
            this.rect = undefined;
            this.isEditing = false;
        }
        get defaultRect() {
            return { x: 0, y: 0, width: 0, height: 0 };
        }
        setup() {
            owl.onWillUpdateProps(() => {
                const isEditing = this.env.model.getters.getEditionMode() !== "inactive";
                if (this.isEditing !== isEditing) {
                    this.isEditing = isEditing;
                    if (!isEditing) {
                        this.rect = undefined;
                        this.env.focusableElement.focus();
                        return;
                    }
                    const position = this.env.model.getters.getPosition();
                    const zone = this.env.model.getters.expandZone(this.env.model.getters.getActiveSheetId(), positionToZone(position));
                    this.rect = this.env.model.getters.getVisibleRect(zone);
                }
            });
        }
        get composerProps() {
            const { width, height } = this.env.model.getters.getSheetViewDimensionWithHeaders();
            return {
                rect: this.rect && { ...this.rect },
                delimitation: {
                    width,
                    height,
                },
                inputStyle: this.composerStyle,
                focus: this.props.focus,
                isDefaultFocus: true,
                onComposerContentFocused: this.props.onComposerContentFocused,
                onComposerCellFocused: this.props.onComposerCellFocused,
                onInputContextMenu: this.props.onInputContextMenu,
            };
        }
        get containerStyle() {
            if (this.env.model.getters.getEditionMode() === "inactive" || !this.rect) {
                return `z-index: -1000;`;
            }
            const isFormula = this.env.model.getters.getCurrentContent().startsWith("=");
            const cell = this.env.model.getters.getActiveCell();
            let style = {};
            if (cell) {
                const cellPosition = this.env.model.getters.getCellPosition(cell.id);
                style = this.env.model.getters.getCellComputedStyle(cellPosition.sheetId, cellPosition.col, cellPosition.row);
            }
            // 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" : 500;
            const fontStyle = !isFormula && style.italic ? "italic" : "normal";
            const textDecoration = !isFormula ? getTextDecoration(style) : "none";
            // align style
            let textAlign = "left";
            if (!isFormula) {
                textAlign = style.align || (cell === null || cell === void 0 ? void 0 : cell.defaultAlign) || "left";
            }
            const sheetDimensions = this.env.model.getters.getSheetViewDimensionWithHeaders();
            const maxWidth = sheetDimensions.width - this.rect.x;
            const maxHeight = sheetDimensions.height - this.rect.y;
            return `
      left: ${left - 1}px;
      top: ${top}px;
      min-width: ${width + 2}px;
      min-height: ${height + 1}px;

      max-width: ${maxWidth}px;
      max-height: ${maxHeight}px;

      background: ${background};
      color: ${color};

      font-size: ${fontSizeMap[fontSize]}px;
      font-weight: ${fontWeight};
      font-style: ${fontStyle};
      text-decoration: ${textDecoration};

      text-align: ${textAlign};
    `;
        }
        get composerStyle() {
            return `
      line-height: ${DEFAULT_CELL_HEIGHT}px;
      max-height: inherit;
      overflow: hidden;
    `;
        }
    }
    GridComposer.template = "o-spreadsheet-GridComposer";
    GridComposer.components = { Composer };

    const CSS$1 = css /* scss */ `
  .o-filter-icon {
    color: ${FILTERS_COLOR};
    position: absolute;
    display: flex;
    align-items: center;
    justify-content: center;
    width: ${FILTER_ICON_EDGE_LENGTH}px;
    height: ${FILTER_ICON_EDGE_LENGTH}px;

    svg {
      path {
        fill: ${FILTERS_COLOR};
      }
    }
  }
  .o-filter-icon:hover {
    background: ${FILTERS_COLOR};
    svg {
      path {
        fill: white;
      }
    }
  }
`;
    class FilterIcon extends owl.Component {
        get style() {
            const { x, y } = this.props.position;
            return `top:${y}px;left:${x}px`;
        }
    }
    FilterIcon.style = CSS$1;
    FilterIcon.template = "o-spreadsheet-FilterIcon";

    const CSS = css /* scss */ ``;
    class FilterIconsOverlay extends owl.Component {
        getVisibleFilterHeaders() {
            const sheetId = this.env.model.getters.getActiveSheetId();
            const headerPositions = this.env.model.getters.getFilterHeaders(sheetId);
            return headerPositions.filter((position) => this.isPositionVisible(position.col, position.row));
        }
        getFilterHeaderPosition(position) {
            const sheetId = this.env.model.getters.getActiveSheetId();
            const rowDims = this.env.model.getters.getRowDimensionsInViewport(sheetId, position.row);
            const colDims = this.env.model.getters.getColDimensionsInViewport(sheetId, position.col);
            // TODO : change this offset when we support vertical cell align
            const centeringOffset = Math.floor((rowDims.size - FILTER_ICON_EDGE_LENGTH) / 2);
            return {
                x: colDims.end - FILTER_ICON_EDGE_LENGTH + this.props.gridPosition.x - FILTER_ICON_MARGIN - 1,
                y: rowDims.end - FILTER_ICON_EDGE_LENGTH + this.props.gridPosition.y - centeringOffset,
            };
        }
        isFilterActive(position) {
            const sheetId = this.env.model.getters.getActiveSheetId();
            return this.env.model.getters.isFilterActive(sheetId, position.col, position.row);
        }
        toggleFilterMenu(position) {
            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",
            });
        }
        isPositionVisible(x, y) {
            const rect = this.env.model.getters.getVisibleRect({
                left: x,
                right: x,
                top: y,
                bottom: y,
            });
            return !(rect.width === 0 || rect.height === 0);
        }
    }
    FilterIconsOverlay.style = CSS;
    FilterIconsOverlay.template = "o-spreadsheet-FilterIconsOverlay";
    FilterIconsOverlay.components = {
        FilterIcon,
    };
    FilterIconsOverlay.defaultProps = {
        gridPosition: { x: 0, y: 0 },
    };

    // -----------------------------------------------------------------------------
    // 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%;

    &: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.ChartAnchor};
      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;
      }
    }
  }
`;
    class FigureComponent extends owl.Component {
        constructor() {
            super(...arguments);
            this.figureRegistry = figureRegistry;
            this.figureRef = owl.useRef("figure");
        }
        get isSelected() {
            return this.env.model.getters.getSelectedFigureId() === this.props.figure.id;
        }
        getBorderWidth() {
            if (this.env.isDashboard())
                return 0;
            return this.isSelected ? ACTIVE_BORDER_WIDTH : BORDER_WIDTH;
        }
        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 style;
        }
        setup() {
            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 === null || el === void 0 ? void 0 : el.focus({ preventScroll: true });
                }
            }, () => [this.env.model.getters.getSelectedFigureId(), this.props.figure.id, this.figureRef.el]);
            owl.onWillUnmount(() => {
                this.props.onFigureDeleted();
            });
        }
        clickAnchor(dirX, dirY, ev) {
            this.props.onClickAnchor(dirX, dirY, ev);
        }
        onMouseDown(ev) {
            this.props.onMouseDown(ev);
        }
        onKeyDown(ev) {
            const figure = this.props.figure;
            const keyDownShortcut = keyboardEventToShortcutString(ev);
            switch (keyDownShortcut) {
                case "Delete":
                    this.env.model.dispatch("DELETE_FIGURE", {
                        sheetId: this.env.model.getters.getActiveSheetId(),
                        id: figure.id,
                    });
                    this.props.onFigureDeleted();
                    ev.preventDefault();
                    ev.stopPropagation();
                    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.preventDefault();
                    ev.stopPropagation();
                    break;
                case "Ctrl+A":
                    // Maybe in the future we will implement a way to select all figures
                    ev.preventDefault();
                    ev.stopPropagation();
                    break;
                case "Ctrl+Y":
                case "Ctrl+Z":
                    if (keyDownShortcut === "Ctrl+Y") {
                        this.env.model.dispatch("REQUEST_REDO");
                    }
                    else if (keyDownShortcut === "Ctrl+Z") {
                        this.env.model.dispatch("REQUEST_UNDO");
                    }
                    ev.preventDefault();
                    ev.stopPropagation();
                    break;
            }
        }
    }
    FigureComponent.template = "o-spreadsheet-FigureComponent";
    FigureComponent.components = {};
    FigureComponent.defaultProps = {
        onFigureDeleted: () => { },
        onMouseDown: () => { },
        onClickAnchor: () => { },
    };
    FigureComponent.props = {
        figure: Object,
        style: { type: String, optional: true },
        onFigureDeleted: { type: Function, optional: true },
        onMouseDown: { type: Function, optional: true },
        onClickAnchor: { type: Function, optional: true },
    };

    /**
     * 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 {
        constructor() {
            super(...arguments);
            this.dnd = owl.useState({
                figId: undefined,
                x: 0,
                y: 0,
                width: 0,
                height: 0,
                cancelDnd: 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();
            });
            owl.onWillUpdateProps(() => {
                const sheetId = this.env.model.getters.getActiveSheetId();
                if (this.dnd.figId && !this.env.model.getters.getFigure(sheetId, this.dnd.figId)) {
                    if (this.dnd.cancelDnd) {
                        this.dnd.cancelDnd();
                    }
                    this.dnd.figId = undefined;
                    this.dnd.cancelDnd = undefined;
                }
            });
        }
        getVisibleFigures() {
            const visibleFigures = this.env.model.getters.getVisibleFigures();
            if (this.dnd.figId && !visibleFigures.some((figure) => figure.id === this.dnd.figId)) {
                const draggedFigure = this.env.model.getters.getFigure(this.env.model.getters.getActiveSheetId(), this.dnd.figId);
                if (draggedFigure) {
                    visibleFigures.push(draggedFigure);
                }
            }
            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.figId) {
                containers.push({
                    type: "dnd",
                    figures: [this.getDndFigure()],
                    style: this.getContainerStyle("dnd"),
                    inverseViewportStyle: this.getInverseViewportPositionStyle("dnd"),
                });
            }
            return containers;
        }
        getContainerStyle(container) {
            const { width: viewWidth, height: viewHeight } = this.env.model.getters.getSheetViewDimension();
            const { x: viewportX, y: viewportY } = this.env.model.getters.getMainViewportCoordinates();
            const left = ["bottomRight", "topRight"].includes(container) ? viewportX : 0;
            const width = viewWidth - left;
            const top = ["bottomRight", "bottomLeft"].includes(container) ? viewportY : 0;
            const height = viewHeight - top;
            return cssPropertiesToCss({
                left: `${left}px`,
                top: `${top}px`,
                width: `${width}px`,
                height: `${height}px`,
            });
        }
        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.figId) {
                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 mouseInitialX = ev.clientX;
            const mouseInitialY = ev.clientY;
            const { x: dndInitialX, y: dndInitialY } = this.internalToScreenCoordinates(figure);
            this.dnd.x = dndInitialX;
            this.dnd.y = dndInitialY;
            this.dnd.width = figure.width;
            this.dnd.height = figure.height;
            const onMouseMove = (ev) => {
                const { x: viewportX, y: viewportY } = this.env.model.getters.getMainViewportCoordinates();
                const { scrollX, scrollY } = this.env.model.getters.getActiveSheetScrollInfo();
                const minX = viewportX ? 0 : -scrollX;
                const minY = viewportY ? 0 : -scrollY;
                this.dnd.figId = figure.id;
                const newX = ev.clientX;
                let deltaX = newX - mouseInitialX;
                this.dnd.x = Math.max(dndInitialX + deltaX, minX);
                const newY = ev.clientY;
                let deltaY = newY - mouseInitialY;
                this.dnd.y = Math.max(dndInitialY + deltaY, minY);
            };
            const onMouseUp = (ev) => {
                if (!this.dnd.figId) {
                    return;
                }
                let { x, y } = this.screenCoordinatesToInternal(this.dnd);
                this.dnd.figId = undefined;
                this.env.model.dispatch("UPDATE_FIGURE", { sheetId, id: figure.id, x, y });
            };
            this.dnd.cancelDnd = startDnd(onMouseMove, onMouseUp);
        }
        startResize(figure, dirX, dirY, ev) {
            ev.stopPropagation();
            const initialX = ev.clientX;
            const initialY = ev.clientY;
            const { scrollX, scrollY } = this.env.model.getters.getActiveSheetScrollInfo();
            const { x: dndInitialX, y: dndInitialY } = this.internalToScreenCoordinates(figure);
            this.dnd.x = dndInitialX;
            this.dnd.y = dndInitialY;
            this.dnd.width = figure.width;
            this.dnd.height = figure.height;
            let onMouseMove;
            onMouseMove = (ev) => {
                this.dnd.figId = figure.id;
                const deltaX = Math.max(dirX * (ev.clientX - initialX), MIN_FIG_SIZE - figure.width);
                const deltaY = Math.max(dirY * (ev.clientY - initialY), MIN_FIG_SIZE - figure.height);
                this.dnd.width = figure.width + deltaX;
                this.dnd.height = figure.height + deltaY;
                if (dirX < 0) {
                    this.dnd.x = dndInitialX - deltaX;
                }
                if (dirY < 0) {
                    this.dnd.y = dndInitialY - deltaY;
                }
                // Adjusts figure dimensions to ensure it remains within header boundaries and viewport during resizing.
                if (this.dnd.x + scrollX <= 0) {
                    this.dnd.width = this.dnd.width + this.dnd.x + scrollX;
                    this.dnd.x = -scrollX;
                }
                if (this.dnd.y + scrollY <= 0) {
                    this.dnd.height = this.dnd.height + this.dnd.y + scrollY;
                    this.dnd.y = -scrollY;
                }
            };
            const onMouseUp = (ev) => {
                if (!this.dnd.figId) {
                    return;
                }
                this.dnd.figId = undefined;
                let { x, y } = this.screenCoordinatesToInternal(this.dnd);
                const update = { x, y };
                if (dirX) {
                    update.width = this.dnd.width;
                }
                if (dirY) {
                    update.height = this.dnd.height;
                }
                this.env.model.dispatch("UPDATE_FIGURE", {
                    sheetId: this.env.model.getters.getActiveSheetId(),
                    id: figure.id,
                    ...update,
                });
            };
            this.dnd.cancelDnd = startDnd(onMouseMove, onMouseUp);
        }
        getDndFigure() {
            const figure = this.getVisibleFigures().find((fig) => fig.id === this.dnd.figId);
            if (!figure)
                throw new Error("Dnd figure not found");
            return {
                ...figure,
                x: this.dnd.x,
                y: this.dnd.y,
                width: this.dnd.width,
                height: this.dnd.height,
            };
        }
        getFigureStyle(figure) {
            if (figure.id !== this.dnd.figId)
                return "";
            return cssPropertiesToCss({
                opacity: "0.9",
                cursor: "grabbing",
            });
        }
        internalToScreenCoordinates({ x, y }) {
            const { x: viewportX, y: viewportY } = this.env.model.getters.getMainViewportCoordinates();
            const { scrollX, scrollY } = this.env.model.getters.getActiveSheetScrollInfo();
            x = x < viewportX ? x : x - scrollX;
            y = y < viewportY ? y : y - scrollY;
            return { x, y };
        }
        screenCoordinatesToInternal({ x, y }) {
            const { x: viewportX, y: viewportY } = this.env.model.getters.getMainViewportCoordinates();
            const { scrollX, scrollY } = this.env.model.getters.getActiveSheetScrollInfo();
            x = viewportX && x < viewportX ? x : x + scrollX;
            y = viewportY && y < viewportY ? y : y + scrollY;
            return { x, y };
        }
    }
    FiguresContainer.template = "o-spreadsheet-FiguresContainer";
    FiguresContainer.components = { FigureComponent };
    figureRegistry.add("chart", { Component: ChartFigure, SidePanelComponent: "ChartPanel" });

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

    function useCellHovered(env, gridRef, callback) {
        let hoveredPosition = {
            col: undefined,
            row: undefined,
        };
        const { Date } = window;
        let x = undefined;
        let y = undefined;
        let lastMoved = 0;
        function getPosition() {
            if (x === undefined || y === undefined) {
                return { col: -1, row: -1 };
            }
            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);
            }
        }
        owl.onMounted(() => {
            const grid = gridRef.el;
            grid.addEventListener("mousemove", updateMousePosition);
            grid.addEventListener("mouseleave", pause);
            grid.addEventListener("mouseenter", resume);
            grid.addEventListener("mousedown", recompute);
        });
        owl.onWillUnmount(() => {
            const grid = gridRef.el;
            grid.removeEventListener("mousemove", updateMousePosition);
            grid.removeEventListener("mouseleave", pause);
            grid.removeEventListener("mouseenter", resume);
            grid.removeEventListener("mousedown", recompute);
        });
        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;
        }
        owl.onMounted(() => {
            gridRef.el.addEventListener("touchstart", onTouchStart);
            gridRef.el.addEventListener("touchend", onTouchEnd);
            gridRef.el.addEventListener("touchmove", onTouchMove);
        });
        owl.onWillUnmount(() => {
            gridRef.el.removeEventListener("touchstart", onTouchStart);
            gridRef.el.removeEventListener("touchend", onTouchEnd);
            gridRef.el.removeEventListener("touchmove", onTouchMove);
        });
    }
    class GridOverlay extends owl.Component {
        setup() {
            this.gridOverlay = owl.useRef("gridOverlay");
            useCellHovered(this.env, this.gridOverlay, this.props.onCellHovered);
            owl.useEffect(() => this.props.onGridResized({
                height: this.gridOverlayEl.clientHeight,
                width: this.gridOverlayEl.clientWidth,
            }), () => [this.gridOverlayEl.clientHeight, this.gridOverlayEl.clientWidth]);
            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;
        }
        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) {
            ev.preventDefault();
            const [col, row] = this.getCartesianCoordinates(ev);
            this.props.onCellRightClicked(col, row, { x: ev.clientX, y: ev.clientY });
        }
        getCartesianCoordinates(ev) {
            const colIndex = this.env.model.getters.getColIndex(ev.offsetX);
            const rowIndex = this.env.model.getters.getRowIndex(ev.offsetY);
            return [colIndex, rowIndex];
        }
    }
    GridOverlay.template = "o-spreadsheet-GridOverlay";
    GridOverlay.components = { FiguresContainer };
    GridOverlay.defaultProps = {
        onCellHovered: () => { },
        onCellDoubleClicked: () => { },
        onCellClicked: () => { },
        onCellRightClicked: () => { },
        onGridResized: () => { },
        onFigureDeleted: () => { },
        sidePanelIsOpen: false,
    };

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

    class AbstractResizer extends owl.Component {
        constructor() {
            super(...arguments);
            this.PADDING = 0;
            this.MAX_SIZE_MARGIN = 0;
            this.MIN_ELEMENT_SIZE = 0;
            this.lastSelectedElementIndex = null;
            this.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.env.model.dispatch(isCtrlKey(ev) ? "PREPARE_SELECTION_INPUT_EXPANSION" : "STOP_SELECTION_INPUT");
                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: ${SCROLLBAR_WIDTH}px;
    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);
    }
  }
`;
    class ColResizer extends AbstractResizer {
        setup() {
            super.setup();
            this.colResizerRef = owl.useRef("colResizer");
            this.PADDING = 15;
            this.MAX_SIZE_MARGIN = 90;
            this.MIN_ELEMENT_SIZE = MIN_COL_WIDTH;
        }
        get sheetId() {
            return this.env.model.getters.getActiveSheetId();
        }
        _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.sheetId, index);
        }
        _getElementSize(index) {
            return this.env.model.getters.getColSize(this.sheetId, 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.sheetId,
                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.sheetId,
                dimension: "COL",
                base: this.state.base,
                elements,
                position: this.state.position,
            });
            if (!result.isSuccessful && result.reasons.includes(2 /* 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.sheetId,
                cols: cols.has(index) ? [...cols] : [index],
            });
        }
        _getType() {
            return "COL";
        }
        _getActiveElements() {
            return this.env.model.getters.getActiveCols();
        }
        _getPreviousVisibleElement(index) {
            const sheetId = this.sheetId;
            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.sheetId,
                elements: hiddenElements,
                dimension: "COL",
            });
        }
        unhideStyleValue(hiddenIndex) {
            return this._getDimensionsInViewport(hiddenIndex).start;
        }
    }
    ColResizer.template = "o-spreadsheet-ColResizer";
    css /* scss */ `
  .o-row-resizer {
    position: absolute;
    top: ${HEADER_HEIGHT}px;
    left: 0;
    bottom: ${SCROLLBAR_WIDTH}px;
    width: ${HEADER_WIDTH}px;
    &.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;
    }
  }
`;
    class RowResizer extends AbstractResizer {
        setup() {
            super.setup();
            this.rowResizerRef = owl.useRef("rowResizer");
            this.PADDING = 5;
            this.MAX_SIZE_MARGIN = 60;
            this.MIN_ELEMENT_SIZE = MIN_ROW_HEIGHT;
        }
        get sheetId() {
            return this.env.model.getters.getActiveSheetId();
        }
        _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.sheetId, index);
        }
        _getElementSize(index) {
            return this.env.model.getters.getRowSize(this.sheetId, 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.sheetId,
                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.sheetId,
                dimension: "ROW",
                base: this.state.base,
                elements,
                position: this.state.position,
            });
            if (!result.isSuccessful && result.reasons.includes(2 /* 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.sheetId,
                rows: rows.has(index) ? [...rows] : [index],
            });
        }
        _getType() {
            return "ROW";
        }
        _getActiveElements() {
            return this.env.model.getters.getActiveRows();
        }
        _getPreviousVisibleElement(index) {
            const sheetId = this.sheetId;
            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.sheetId,
                dimension: "ROW",
                elements: hiddenElements,
            });
        }
        unhideStyleValue(hiddenIndex) {
            return this._getDimensionsInViewport(hiddenIndex).start;
        }
    }
    RowResizer.template = "o-spreadsheet-RowResizer";
    css /* scss */ `
  .o-overlay {
    .all {
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      width: ${HEADER_WIDTH}px;
      height: ${HEADER_HEIGHT}px;
    }
  }
`;
    class HeadersOverlay extends owl.Component {
        selectAll() {
            this.env.model.selection.selectAll();
        }
    }
    HeadersOverlay.template = "o-spreadsheet-HeadersOverlay";
    HeadersOverlay.components = { ColResizer, RowResizer };

    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;`);
            if (width === 0 || height === 0) {
                return;
            }
            // 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 {
        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 `
        left:${leftValue}px;
        top:${topValue}px;
        width:${widthValue}px;
        height:${heightValue}px;
    `;
        }
        onMouseDown(ev) {
            this.props.onMoveHighlight(ev.clientX, ev.clientY);
        }
    }
    Border.template = "o-spreadsheet-Border";

    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 {
        constructor() {
            super(...arguments);
            this.isTop = this.props.orientation[0] === "n";
            this.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 `
      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.template = "o-spreadsheet-Corner";

    css /*SCSS*/ `
  .o-highlight {
    z-index: ${ComponentsImportance.Highlight};
  }
`;
    class Highlight extends owl.Component {
        constructor() {
            super(...arguments);
            this.highlightState = owl.useState({
                shiftingMode: "none",
            });
        }
        onResizeHighlight(isLeft, isTop) {
            const activeSheet = this.env.model.getters.getActiveSheet();
            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", {
                range: this.env.model.getters.getRangeDataFromZone(activeSheet.id, currentZone),
            });
            const mouseMove = (col, row) => {
                if (lastCol !== col || lastRow !== row) {
                    const activeSheetId = this.env.model.getters.getActiveSheetId();
                    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),
                    };
                    newZone = this.env.model.getters.expandZone(activeSheetId, newZone);
                    if (!isEqual(newZone, currentZone)) {
                        this.env.model.dispatch("CHANGE_HIGHLIGHT", {
                            range: this.env.model.getters.getRangeDataFromZone(activeSheet.id, newZone),
                        });
                        currentZone = newZone;
                    }
                }
            };
            const mouseUp = () => {
                this.highlightState.shiftingMode = "none";
                // To do:
                // Command used here to restore focus to the current composer,
                // to be changed when refactoring the 'edition' plugin
                this.env.model.dispatch("STOP_COMPOSER_RANGE_SELECTION");
            };
            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", {
                range: this.env.model.getters.getRangeDataFromZone(activeSheetId, 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,
                    };
                    newZone = this.env.model.getters.expandZone(activeSheetId, newZone);
                    if (!isEqual(newZone, currentZone)) {
                        this.env.model.dispatch("CHANGE_HIGHLIGHT", {
                            range: this.env.model.getters.getRangeDataFromZone(activeSheetId, newZone),
                        });
                        currentZone = newZone;
                    }
                }
            };
            const mouseUp = () => {
                this.highlightState.shiftingMode = "none";
                // To do:
                // Command used here to restore focus to the current composer,
                // to be changed when refactoring the 'edition' plugin
                this.env.model.dispatch("STOP_COMPOSER_RANGE_SELECTION");
            };
            dragAndDropBeyondTheViewport(this.env, mouseMove, mouseUp);
        }
    }
    Highlight.template = "o-spreadsheet-Highlight";
    Highlight.components = {
        Corner,
        Border,
    };

    class ScrollBar$1 {
        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 {
        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.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>
  `;
    ScrollBar.defaultProps = {
        width: 1,
        height: 1,
    };

    class HorizontalScrollBar extends owl.Component {
        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.position.left + 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.components = { ScrollBar };
    HorizontalScrollBar.template = owl.xml /*xml*/ `
      <ScrollBar
        t-if="isDisplayed"
        width="width"
        position="position"
        offset="offset"
        direction="'horizontal'"
        onScroll.bind="onScroll"
      />`;
    HorizontalScrollBar.defaultProps = {
        position: { left: 0 },
    };

    class VerticalScrollBar extends owl.Component {
        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.position.top + 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.components = { ScrollBar };
    VerticalScrollBar.template = owl.xml /*xml*/ `
    <ScrollBar
      t-if="isDisplayed"
      height="height"
      position="position"
      offset="offset"
      direction="'vertical'"
      onScroll.bind="onScroll"
    />`;
    VerticalScrollBar.defaultProps = {
        position: { top: 0 },
    };

    const registries$1 = {
        ROW: rowMenuRegistry,
        COL: colMenuRegistry,
        CELL: cellMenuRegistry,
    };
    // -----------------------------------------------------------------------------
    // JS
    // -----------------------------------------------------------------------------
    class Grid extends owl.Component {
        constructor() {
            super(...arguments);
            this.HEADER_HEIGHT = HEADER_HEIGHT;
            this.HEADER_WIDTH = HEADER_WIDTH;
            // this map will handle most of the actions that should happen on key down. The arrow keys are managed in the key
            // down itself
            this.keyDownMapping = {
                ENTER: () => {
                    const cell = this.env.model.getters.getActiveCell();
                    !cell || cell.isEmpty()
                        ? 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 || cell.isEmpty()
                        ? 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(),
                    });
                },
                "CTRL+A": () => this.env.model.selection.loopSelection(),
                "CTRL+S": () => {
                    var _a, _b;
                    (_b = (_a = this.props).onSaveRequested) === null || _b === void 0 ? void 0 : _b.call(_a);
                },
                "CTRL+Z": () => this.env.model.dispatch("REQUEST_UNDO"),
                "CTRL+Y": () => 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 },
                }),
                "ALT+=": () => {
                    var _a;
                    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 = (_a = sums[0]) === null || _a === void 0 ? void 0 : _a.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");
                    }
                },
                "CTRL+HOME": () => {
                    const sheetId = this.env.model.getters.getActiveSheetId();
                    const { col, row } = this.env.model.getters.getNextVisibleCellPosition(sheetId, 0, 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.getPosition();
                    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.getPosition();
                    this.env.model.selection.selectZone({ cell: position, zone: newZone });
                },
                "CTRL+SHIFT+ ": () => {
                    this.env.model.selection.selectAll();
                },
                "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"),
            };
        }
        setup() {
            this.menuState = owl.useState({
                isOpen: false,
                position: null,
                menuItems: [],
            });
            this.gridRef = owl.useRef("grid");
            this.canvasPosition = useAbsolutePosition(this.gridRef);
            this.hoveredCell = owl.useState({ col: undefined, row: undefined });
            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 `
      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() {
            this.closeOpenedPopover();
            this.focusDefaultElement();
        }
        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,
            };
        }
        isAutoFillActive() {
            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: Math.max(scrollX + deltaX, 0),
                offsetY: Math.max(scrollY + deltaY, 0),
            });
        }
        getClientPositionKey(client) {
            var _a, _b, _c;
            return `${client.id}-${(_a = client.position) === null || _a === void 0 ? void 0 : _a.sheetId}-${(_b = client.position) === null || _b === void 0 ? void 0 : _b.col}-${(_c = client.position) === null || _c === void 0 ? void 0 : _c.row}`;
        }
        isCellHovered(col, row) {
            return this.hoveredCell.col === col && this.hoveredCell.row === row;
        }
        // ---------------------------------------------------------------------------
        // Zone selection with mouse
        // ---------------------------------------------------------------------------
        onCellClicked(col, row, { addZone, expandZone }) {
            if (addZone) {
                this.env.model.dispatch("PREPARE_SELECTION_INPUT_EXPANSION");
            }
            this.closeOpenedPopover();
            if (this.env.model.getters.getEditionMode() === "editing") {
                this.env.model.dispatch("STOP_EDITION");
            }
            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) => {
                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 = () => {
                this.env.model.dispatch("STOP_SELECTION_INPUT");
                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.getCell(sheetId, col, row);
            if (!cell || cell.isEmpty()) {
                this.props.onGridComposerCellFocused();
            }
            else {
                this.props.onComposerContentFocused();
            }
        }
        closeOpenedPopover() {
            this.env.model.dispatch("CLOSE_CELL_POPOVER");
        }
        // ---------------------------------------------------------------------------
        // Keyboard interactions
        // ---------------------------------------------------------------------------
        processArrows(ev) {
            ev.preventDefault();
            ev.stopPropagation();
            this.closeOpenedPopover();
            const arrowMap = {
                ArrowDown: "down",
                ArrowLeft: "left",
                ArrowRight: "right",
                ArrowUp: "up",
            };
            const direction = arrowMap[ev.key];
            if (ev.shiftKey) {
                this.env.model.selection.resizeAnchorZone(direction, isCtrlKey(ev) ? "end" : 1);
            }
            else {
                this.env.model.selection.moveAnchorCell(direction, isCtrlKey(ev) ? "end" : 1);
            }
            if (this.env.model.getters.isPaintingFormat()) {
                this.env.model.dispatch("PASTE", {
                    target: this.env.model.getters.getSelectedZones(),
                });
            }
        }
        onKeydown(ev) {
            if (ev.key.startsWith("Arrow")) {
                this.processArrows(ev);
                return;
            }
            let keyDownString = "";
            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;
            }
        }
        // ---------------------------------------------------------------------------
        // Context Menu
        // ---------------------------------------------------------------------------
        onInputContextMenu(ev) {
            ev.preventDefault();
            const lastZone = this.env.model.getters.getSelectedZone();
            const { left: col, top: row } = lastZone;
            let type = "CELL";
            this.env.model.dispatch("STOP_EDITION");
            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);
            this.toggleContextMenu(type, this.canvasPosition.x + x + width, this.canvasPosition.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) {
            this.closeOpenedPopover();
            this.menuState.isOpen = true;
            this.menuState.position = { x, y };
            this.menuState.menuItems = registries$1[type].getAll();
        }
        copy(cut, ev) {
            if (!this.gridEl.contains(document.activeElement)) {
                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();
            ev.clipboardData.setData("text/plain", content);
            ev.preventDefault();
        }
        paste(ev) {
            if (!this.gridEl.contains(document.activeElement)) {
                return;
            }
            const clipboardData = ev.clipboardData;
            if (clipboardData.types.indexOf("text/plain") > -1) {
                const content = clipboardData.getData("text/plain");
                const target = this.env.model.getters.getSelectedZones();
                const clipBoardString = this.env.model.getters.getClipboardContent();
                if (clipBoardString === content) {
                    // the paste actually comes from o-spreadsheet itself
                    interactivePaste(this.env, target);
                }
                else {
                    interactivePasteFromOS(this.env, target, content);
                }
            }
        }
        closeMenu() {
            this.menuState.isOpen = false;
            this.focusDefaultElement();
        }
    }
    Grid.template = "o-spreadsheet-Grid";
    Grid.components = {
        GridComposer,
        GridOverlay,
        GridPopover,
        HeadersOverlay,
        Menu,
        Autofill,
        ClientTag,
        Highlight,
        Popover,
        VerticalScrollBar,
        HorizontalScrollBar,
        FilterIconsOverlay,
    };

    /**
     * Abstract base implementation of a cell.
     * Concrete cell classes are responsible to build the raw cell `content` based on
     * whatever data they have (formula, string, ...).
     */
    class AbstractCell {
        constructor(id, lazyEvaluated, properties) {
            this.id = id;
            this.style = properties.style;
            this.format = properties.format;
            this.lazyEvaluated = lazyEvaluated.map((evaluated) => ({
                ...evaluated,
                format: properties.format || evaluated.format,
            }));
        }
        isFormula() {
            return false;
        }
        isLink() {
            return false;
        }
        isEmpty() {
            return false;
        }
        get evaluated() {
            return this.lazyEvaluated();
        }
        get formattedValue() {
            return formatValue(this.evaluated.value, this.evaluated.format);
        }
        get composerContent() {
            return this.content;
        }
        get defaultAlign() {
            switch (this.evaluated.type) {
                case CellValueType.number:
                case CellValueType.empty:
                    return "right";
                case CellValueType.boolean:
                case CellValueType.error:
                    return "center";
                case CellValueType.text:
                    return "left";
            }
        }
        /**
         * Only empty cells, text cells and numbers are valid
         */
        get isAutoSummable() {
            var _a;
            switch (this.evaluated.type) {
                case CellValueType.empty:
                case CellValueType.text:
                    return true;
                case CellValueType.number:
                    return !((_a = this.evaluated.format) === null || _a === void 0 ? void 0 : _a.match(DATETIME_FORMAT));
                case CellValueType.error:
                case CellValueType.boolean:
                    return false;
            }
        }
    }
    class EmptyCell extends AbstractCell {
        constructor(id, properties = {}) {
            super(id, lazy({ value: "", type: CellValueType.empty }), properties);
            this.content = "";
        }
        isEmpty() {
            return true;
        }
    }
    class NumberCell extends AbstractCell {
        constructor(id, value, properties = {}) {
            super(id, lazy({ value, type: CellValueType.number }), properties);
            this.content = formatValue(this.evaluated.value);
        }
        get composerContent() {
            var _a;
            if ((_a = this.format) === null || _a === void 0 ? void 0 : _a.includes("%")) {
                return `${this.evaluated.value * 100}%`;
            }
            return super.composerContent;
        }
    }
    class BooleanCell extends AbstractCell {
        constructor(id, value, properties = {}) {
            super(id, lazy({ value, type: CellValueType.boolean }), properties);
            this.content = this.evaluated.value ? "TRUE" : "FALSE";
        }
    }
    class TextCell extends AbstractCell {
        constructor(id, value, properties = {}) {
            super(id, lazy({ value, type: CellValueType.text }), properties);
            this.content = this.evaluated.value;
        }
    }
    /**
     * A date time cell is a number cell with a required
     * date time format.
     */
    class DateTimeCell extends NumberCell {
        constructor(id, value, properties) {
            super(id, value, properties);
            this.format = properties.format;
        }
        get composerContent() {
            return formatValue(this.evaluated.value, this.format);
        }
    }
    class LinkCell extends AbstractCell {
        constructor(id, content, properties = {}) {
            const link = parseMarkdownLink(content);
            link.label = _t(link.label);
            super(id, lazy({ value: link.label, type: CellValueType.text }), properties);
            this.link = link;
            this.content = content;
        }
        isLink() {
            return true;
        }
        get composerContent() {
            return this.link.label;
        }
    }
    /**
     * Simple web link cell
     */
    class WebLinkCell extends LinkCell {
        constructor(id, content, properties = {}) {
            super(id, content, properties);
            this.link.url = this.withHttp(this.link.url);
            this.link.isExternal = true;
            this.content = markdownLink(this.link.label, this.link.url);
            this.urlRepresentation = this.link.url;
            this.isUrlEditable = true;
        }
        action(env) {
            window.open(this.link.url, "_blank");
        }
        /**
         * Add the `https` prefix to the url if it's missing
         */
        withHttp(url) {
            return !/^https?:\/\//i.test(url) ? `https://${url}` : url;
        }
    }
    /**
     * Link redirecting to a given sheet in the workbook.
     */
    class SheetLinkCell extends LinkCell {
        constructor(id, content, properties = {}, sheetName) {
            super(id, content, properties);
            this.sheetName = sheetName;
            this.sheetId = parseSheetLink(this.link.url);
            this.isUrlEditable = false;
        }
        action(env) {
            env.model.dispatch("ACTIVATE_SHEET", {
                sheetIdFrom: env.model.getters.getActiveSheetId(),
                sheetIdTo: this.sheetId,
            });
        }
        get urlRepresentation() {
            return this.sheetName(this.sheetId) || _lt("Invalid sheet");
        }
    }
    class FormulaCell extends AbstractCell {
        constructor(buildFormulaString, id, compiledFormula, dependencies, properties) {
            super(id, lazy({ value: LOADING, type: CellValueType.text }), properties);
            this.buildFormulaString = buildFormulaString;
            this.compiledFormula = compiledFormula;
            this.dependencies = dependencies;
        }
        get content() {
            return this.buildFormulaString(this);
        }
        isFormula() {
            return true;
        }
        assignEvaluation(lazyEvaluationResult) {
            this.lazyEvaluated = lazyEvaluationResult.map((evaluationResult) => {
                if (evaluationResult instanceof EvaluationError) {
                    return {
                        value: evaluationResult.errorType,
                        type: CellValueType.error,
                        error: evaluationResult,
                    };
                }
                const { value, format } = evaluationResult;
                switch (typeof value) {
                    case "number":
                        return {
                            value: value || 0,
                            format,
                            type: CellValueType.number,
                        };
                    case "boolean":
                        return {
                            value,
                            format,
                            type: CellValueType.boolean,
                        };
                    case "string":
                        return {
                            value,
                            format,
                            type: CellValueType.text,
                        };
                    case "object": // null
                        return {
                            value: 0,
                            format,
                            type: CellValueType.number,
                        };
                    default:
                        // cannot happen with Typescript compiler watching
                        // but possible in a vanilla javascript code base
                        return {
                            value: "",
                            type: CellValueType.empty,
                        };
                }
            });
        }
    }
    /**
     * Cell containing a formula which could not be compiled
     * or a content which could not be parsed.
     */
    class ErrorCell extends AbstractCell {
        /**
         * @param id
         * @param content Invalid formula string
         * @param error Compilation or parsing error
         * @param properties
         */
        constructor(id, content, error, properties) {
            super(id, lazy({
                value: error.errorType,
                type: CellValueType.error,
                error,
            }), properties);
            this.content = content;
        }
    }

    cellRegistry
        .add("Formula", {
        sequence: 10,
        match: (content) => content.startsWith("="),
        createCell: (id, content, properties, sheetId, getters) => {
            const compiledFormula = compile(content);
            const dependencies = compiledFormula.dependencies.map((xc) => getters.getRangeFromSheetXC(sheetId, xc));
            return new FormulaCell((cell) => getters.buildFormulaContent(sheetId, cell), id, compiledFormula, dependencies, properties);
        },
    })
        .add("Empty", {
        sequence: 20,
        match: (content) => content === "",
        createCell: (id, content, properties) => new EmptyCell(id, properties),
    })
        .add("NumberWithDateTimeFormat", {
        sequence: 25,
        match: (content, format) => !!format && isNumber(content) && isDateTimeFormat(format),
        createCell: (id, content, properties) => {
            const format = properties.format;
            return new DateTimeCell(id, parseNumber(content), { ...properties, format });
        },
    })
        .add("Number", {
        sequence: 30,
        match: (content) => isNumber(content),
        createCell: (id, content, properties) => {
            if (!properties.format) {
                properties.format = detectNumberFormat(content);
            }
            return new NumberCell(id, parseNumber(content), properties);
        },
    })
        .add("Boolean", {
        sequence: 40,
        match: (content) => isBoolean(content),
        createCell: (id, content, properties) => {
            return new BooleanCell(id, content.toUpperCase() === "TRUE" ? true : false, properties);
        },
    })
        .add("DateTime", {
        sequence: 50,
        match: (content) => isDateTime(content),
        createCell: (id, content, properties) => {
            const internalDate = parseDateTime(content);
            const format = properties.format || internalDate.format;
            return new DateTimeCell(id, internalDate.value, { ...properties, format });
        },
    })
        .add("MarkdownSheetLink", {
        sequence: 60,
        match: (content) => isMarkdownSheetLink(content),
        createCell: (id, content, properties, sheetId, getters) => {
            return new SheetLinkCell(id, content, properties, (sheetId) => getters.tryGetSheetName(sheetId));
        },
    })
        .add("MarkdownLink", {
        sequence: 70,
        match: (content) => isMarkdownLink(content),
        createCell: (id, content, properties) => {
            return new WebLinkCell(id, content, properties);
        },
    })
        .add("WebLink", {
        sequence: 80,
        match: (content) => isWebLink(content),
        createCell: (id, content, properties) => {
            return new WebLinkCell(id, markdownLink(content, content), properties);
        },
    });
    /**
     * Return a factory function which can instantiate cells of
     * different types, based on a raw content.
     *
     * ```
     * // the createCell function can be used to instantiate new cells
     * const createCell = cellFactory(getters);
     * const cell = createCell(id, cellContent, cellProperties, sheetId)
     * ```
     */
    function cellFactory(getters) {
        const builders = cellRegistry.getAll().sort((a, b) => a.sequence - b.sequence);
        return function createCell(id, content, properties, sheetId) {
            const builder = builders.find((factory) => factory.match(content, properties.format));
            if (!builder) {
                return new TextCell(id, content, properties);
            }
            try {
                return builder.createCell(id, content, properties, sheetId, getters);
            }
            catch (error) {
                return new ErrorCell(id, content, error instanceof EvaluationError
                    ? error
                    : new BadExpressionError(error.message || DEFAULT_ERROR_MESSAGE), properties);
            }
        };
    }
    function detectNumberFormat(content) {
        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;
    }

    /**
     * Parse a string representing a primitive cell value
     */
    function parsePrimitiveContent(content) {
        if (content === "") {
            return "";
        }
        else if (isNumber(content)) {
            return parseNumber(content);
        }
        else if (isBoolean(content)) {
            return content.toUpperCase() === "TRUE" ? true : false;
        }
        else if (isDateTime(content)) {
            return parseDateTime(content).value;
        }
        else {
            return content;
        }
    }

    /**
     * Represent a raw XML string
     */
    class 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 = {
        arrows: "3Arrows",
        smiley: "3Symbols",
        dots: "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",
    };
    const RELATIONSHIP_NSR = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
    const HEIGHT_FACTOR = 0.75; // 100px => 75 u
    const WIDTH_FACTOR = 0.1317; // 100px => 13.17 u
    /** 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",
        "BYCOL",
        "BYROW",
        "CEILING.MATH",
        "CEILING.PRECISE",
        "CHISQ.DIST",
        "CHISQ.DIST.RT",
        "CHISQ.INV",
        "CHISQ.INV.RT",
        "CHISQ.TEST",
        "CHOOSECOLS",
        "CHOOSEROWS",
        "COMBINA",
        "CONCAT",
        "CONFIDENCE.NORM",
        "CONFIDENCE.T",
        "COT",
        "COTH",
        "COVARIANCE.P",
        "COVARIANCE.S",
        "CSC",
        "CSCH",
        "DAYS",
        "DECIMAL",
        "DROP",
        "ERF.PRECISE",
        "ERFC.PRECISE",
        "EXPAND",
        "EXPON.DIST",
        "F.DIST",
        "F.DIST.RT",
        "F.INV",
        "F.INV.RT",
        "F.TEST",
        "FIELDVALUE",
        "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",
        "HSTACK",
        "HYPGEOM.DIST",
        "IFNA",
        "IFS",
        "IMCOSH",
        "IMCOT",
        "IMCSC",
        "IMCSCH",
        "IMSEC",
        "IMSECH",
        "IMSINH",
        "IMTAN",
        "ISFORMULA",
        "ISOMITTED",
        "ISOWEEKNUM",
        "LAMBDA",
        "LET",
        "LOGNORM.DIST",
        "LOGNORM.INV",
        "MAKEARRAY",
        "MAP",
        "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",
        "PQSOURCE",
        "PYTHON_STR",
        "PYTHON_TYPE",
        "PYTHON_TYPENAME",
        "QUARTILE.EXC",
        "QUARTILE.INC",
        "QUERYSTRING",
        "RANDARRAY",
        "RANK.AVG",
        "RANK.EQ",
        "REDUCE",
        "RRI",
        "SCAN",
        "SEC",
        "SECH",
        "SEQUENCE",
        "SHEET",
        "SHEETS",
        "SKEW.P",
        "SORTBY",
        "STDEV.P",
        "STDEV.S",
        "SWITCH",
        "T.DIST",
        "T.DIST.2T",
        "T.DIST.RT",
        "T.INV",
        "T.INV.2T",
        "T.TEST",
        "TAKE",
        "TEXTAFTER",
        "TEXTBEFORE",
        "TEXTJOIN",
        "TEXTSPLIT",
        "TOCOL",
        "TOROW",
        "UNICHAR",
        "UNICODE",
        "UNIQUE",
        "VAR.P",
        "VAR.S",
        "VSTACK",
        "WEBSERVICE",
        "WEIBULL.DIST",
        "WRAPCOLS",
        "WRAPROWS",
        "XLOOKUP",
        "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 {
        constructor() {
            this._parsingWarnings = new Set();
            this._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_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: "thin",
        dotted: "thin",
        double: "thin",
        hair: "thin",
        medium: "thin",
        mediumDashDot: "thin",
        mediumDashDotDot: "thin",
        mediumDashed: "thin",
        none: undefined,
        slantDashDot: "thin",
        thick: "thin",
        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",
    };
    /** 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 = {
        none: "none",
        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
    };

    /**
     * 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) {
        var _a, _b, _c;
        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] || ((_a = formats.find((f) => f.id === numFmtId)) === null || _a === void 0 ? void 0 : _a.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 = ((_b = convertedFormat.match(/"/g)) === null || _b === void 0 ? void 0 : _b.length) || 0;
                const numberOfOpenBrackets = ((_c = convertedFormat.match(/\[/g)) === null || _c === void 0 ? void 0 : _c.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);
            return true;
        }
        catch (e) {
            return false;
        }
    }
    function isXlsxDateFormat(format) {
        return format.match(XLSX_DATE_FORMAT_REGEX) !== null;
    }
    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|mmmm|mmm/g, "mm");
        format = format.replace(/dddd|ddd/g, "dd");
        format = format.replace(/am\/pm|a\/m/g, "a");
        format = format.replace(/\byy\b/g, "yyyy");
        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, 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) {
        var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
        addStyleWarnings(styleStruct === null || styleStruct === void 0 ? void 0 : styleStruct.fontStyle, styleStruct === null || styleStruct === void 0 ? void 0 : styleStruct.fillStyle, warningManager);
        addHorizontalAlignmentWarnings((_a = styleStruct === null || styleStruct === void 0 ? void 0 : styleStruct.alignment) === null || _a === void 0 ? void 0 : _a.horizontal, warningManager);
        addVerticalAlignmentWarnings((_b = styleStruct === null || styleStruct === void 0 ? void 0 : styleStruct.alignment) === null || _b === void 0 ? void 0 : _b.vertical, warningManager);
        return {
            bold: (_c = styleStruct.fontStyle) === null || _c === void 0 ? void 0 : _c.bold,
            italic: (_d = styleStruct.fontStyle) === null || _d === void 0 ? void 0 : _d.italic,
            strikethrough: (_e = styleStruct.fontStyle) === null || _e === void 0 ? void 0 : _e.strike,
            underline: (_f = styleStruct.fontStyle) === null || _f === void 0 ? void 0 : _f.underline,
            align: ((_g = styleStruct.alignment) === null || _g === void 0 ? void 0 : _g.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: ((_h = styleStruct.fillStyle) === null || _h === void 0 ? void 0 : _h.patternType) === "solid"
                ? convertColor((_j = styleStruct.fillStyle) === null || _j === void 0 ? void 0 : _j.fgColor)
                : convertColor((_k = styleStruct.fillStyle) === null || _k === void 0 ? void 0 : _k.bgColor),
            textColor: convertColor((_l = styleStruct.fontStyle) === null || _l === void 0 ? void 0 : _l.color),
            fontSize: ((_m = styleStruct.fontStyle) === null || _m === void 0 ? void 0 : _m.size)
                ? getClosestFontSize(styleStruct.fontStyle.size)
                : undefined,
        };
    }
    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);
    }
    /**
     * We currently only support only a set of font sizes, we cannot define new font sizes.
     * This function adapts an arbitrary font size to the closest supported font size.
     */
    function getClosestFontSize(fontSize) {
        const supportedSizes = Object.keys(fontSizeMap).map(Number);
        const closest = supportedSizes.reduce((prev, curr) => Math.abs(curr - fontSize) < Math.abs(prev - fontSize) ? curr : prev);
        return closest;
    }
    // ---------------------------------------------------------------------------
    // 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) {
            warningManager.generateNotSupportedWarning(WarningTypes.VerticalAlignmentNotSupported);
        }
    }

    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 getCellType(value) {
        switch (typeof value) {
            case "boolean":
                return "b";
            case "string":
                return "str";
            case "number":
                return "n";
        }
    }
    /**
     * For some reason, Excel will only take the devicePixelRatio (i.e. interface scale on Windows desktop)
     * into account for the height.
     */
    function convertHeightToExcel(height) {
        return Math.round(HEIGHT_FACTOR * height * window.devicePixelRatio * 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 extractStyle(cell, data) {
        let style = {};
        if (cell.style) {
            style = data.styles[cell.style];
        }
        const format = extractFormat(cell, data);
        const styles = {
            font: {
                size: (style === null || style === void 0 ? void 0 : style.fontSize) || DEFAULT_FONT_SIZE,
                color: { rgb: (style === null || style === void 0 ? void 0 : style.textColor) ? style.textColor : "000000" },
                family: 2,
                name: "Arial",
            },
            fill: (style === null || style === void 0 ? void 0 : style.fillColor)
                ? {
                    fgColor: { rgb: style.fillColor },
                }
                : { reservedAttribute: "none" },
            numFmt: format ? { format: format, id: 0 /* id not used for export */ } : undefined,
            border: cell.border || 0,
            alignment: {
                vertical: "center",
                horizontal: style.align,
            },
        };
        styles.font["strike"] = !!(style === null || style === void 0 ? void 0 : style.strikethrough) || undefined;
        styles.font["underline"] = !!(style === null || style === void 0 ? void 0 : style.underline) || undefined;
        styles.font["bold"] = !!(style === null || style === void 0 ? void 0 : style.bold) || undefined;
        styles.font["italic"] = !!(style === null || style === void 0 ? void 0 : style.italic) || undefined;
        return styles;
    }
    function extractFormat(cell, data) {
        if (cell.format) {
            return data.formats[cell.format];
        }
        if (cell.isFormula) {
            const tokens = tokenize(cell.content || "");
            const functions = functionRegistry.content;
            const isExported = tokens
                .filter((tk) => tk.type === "FUNCTION")
                .every((tk) => functions[tk.value.toUpperCase()].isExported);
            if (!isExported) {
                return cell.computedFormat;
            }
        }
        return undefined;
    }
    function normalizeStyle(construct, styles) {
        // Normalize this
        const numFmtId = convertFormat(styles["numFmt"], construct.numFmts);
        const style = {
            fontId: pushElement(styles.font, construct.fonts),
            fillId: pushElement(styles.fill, construct.fills),
            borderId: styles.border,
            numFmtId,
            alignment: {
                vertical: styles.alignment.vertical,
                horizontal: styles.alignment.horizontal,
            },
        };
        return pushElement(style, construct.styles);
    }
    function convertFormat(format, numFmtStructure) {
        if (!format) {
            return 0;
        }
        let formatId = XLSX_FORMAT_MAP[format.format];
        if (!formatId) {
            formatId = pushElement(format, numFmtStructure) + 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) {
        let len = propertyList.length;
        const operator = typeof property === "object" ? deepEquals : (a, b) => a === b;
        for (let i = 0; i < len; i++) {
            if (operator(property, propertyList[i])) {
                return i;
            }
        }
        propertyList[propertyList.length] = property;
        return propertyList.length - 1;
    }
    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;
    }
    /**
     * 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) => isSheetNameEqual(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) {
        var _a;
        let position = 0;
        for (let i = 0; i < colIndex; i++) {
            const colAtIndex = sheetData.cols.find((col) => i >= col.min && i <= col.max);
            if (colAtIndex === null || colAtIndex === void 0 ? void 0 : colAtIndex.width) {
                position += colAtIndex.width;
            }
            else if ((_a = sheetData.sheetFormat) === null || _a === void 0 ? void 0 : _a.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) {
        var _a;
        let position = 0;
        for (let i = 0; i < rowIndex; i++) {
            const rowAtIndex = sheetData.rows[i];
            if (rowAtIndex === null || rowAtIndex === void 0 ? void 0 : rowAtIndex.height) {
                position += rowAtIndex.height;
            }
            else if ((_a = sheetData.sheetFormat) === null || _a === void 0 ? void 0 : _a.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 width = x2 - x1;
        const height = y2 - y1;
        const chartData = convertChartData(figure.data);
        if (!chartData)
            return undefined;
        return {
            id: id,
            x: x1,
            y: y1,
            width: width,
            height: height,
            tag: "chart",
            data: convertChartData(figure.data),
        };
    }
    function convertChartData(chartData) {
        var _a;
        if (chartData.dataSets.length === 0) {
            return undefined;
        }
        const labelRange = (_a = chartData.dataSets[0].label) === null || _a === void 0 ? void 0 : _a.replace(/\$/g, "");
        let dataSets = chartData.dataSets.map((data) => data.range.replace(/\$/g, ""));
        // For doughnut charts, in chartJS first dataset = outer dataset, in excel first dataset = inner dataset
        if (chartData.type === "pie") {
            dataSets.reverse();
        }
        return {
            dataSets,
            dataSetsHaveTitle: false,
            labelRange,
            title: chartData.title || "",
            type: chartData.type,
            background: convertColor({ rgb: chartData.backgroundColor }) || "#FFFFFF",
            verticalAxisPosition: chartData.verticalAxisPosition,
            legendPosition: chartData.legendPosition,
            stacked: chartData.stacked || false,
            cumulative: chartData.cumulative || false,
            labelsAsText: false,
        };
    }

    /**
     * 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 === null || cell === void 0 ? void 0 : 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) => {
            var _a;
            externalRefId = Number(externalRefId) - 1;
            cellRef = cellRef.replace(/\$/g, "");
            const sheetIndex = data.externalBooks[externalRefId].sheetNames.findIndex((name) => isSheetNameEqual(name, sheetName));
            if (sheetIndex === -1) {
                return match;
            }
            const externalDataset = (_a = data.externalBooks[externalRefId].datasets.find((dataset) => dataset.sheetId === sheetIndex)) === null || _a === void 0 ? void 0 : _a.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) {
        var _a;
        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 ((_a = sheet.sheetFormat) === null || _a === void 0 ? void 0 : _a.defaultColWidth)
                colSize = sheet.sheetFormat.defaultColWidth;
            else
                colSize = EXCEL_DEFAULT_COL_WIDTH;
            cols[i - 1] = { size: convertWidthFromExcel(colSize), isHidden: col === null || col === void 0 ? void 0 : col.hidden };
        }
        return cols;
    }
    function convertRows(sheet, numberOfRows) {
        var _a;
        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 ((_a = sheet.sheetFormat) === null || _a === void 0 ? void 0 : _a.defaultRowHeight)
                rowSize = sheet.sheetFormat.defaultRowHeight;
            else
                rowSize = EXCEL_DEFAULT_ROW_HEIGHT;
            rows[i - 1] = { size: convertHeightFromExcel(rowSize), isHidden: row === null || row === void 0 ? void 0 : 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], largeMax(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 = ["thin", "#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) {
        var _a, _b, _c, _d;
        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 ((_a = table.style) === null || _a === void 0 ? void 0 : _a.showFirstColumn) {
                    applyStyleToZone(TABLE_HIGHLIGHTED_CELL_STYLE, { ...tableZone, right: tableZone.left }, sheet.cells, styles);
                }
                if ((_b = table.style) === null || _b === void 0 ? void 0 : _b.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 || ((_c = table.style) === null || _c === void 0 ? void 0 : _c.showColumnStripes)
                                ? TABLE_BORDER_STYLE
                                : undefined,
                            right: col === tableZone.right ? TABLE_BORDER_STYLE : undefined,
                            top: row === tableZone.top ||
                                ((_d = table.style) === null || _d === void 0 ? void 0 : _d.showRowStripes) ||
                                row > tableZone.bottom - table.totalsRowCount
                                ? TABLE_BORDER_STYLE
                                : undefined,
                            bottom: row === tableZone.bottom ? TABLE_BORDER_STYLE : undefined,
                        };
                        const newBorder = (cell === null || cell === void 0 ? void 0 : 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 === null || cell === void 0 ? void 0 : 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) {
        let deconstructedSheets = null;
        for (let tableSheet of convertedSheets) {
            const tables = xlsxSheets.find((s) => isSheetNameEqual(s.sheetName, tableSheet.name)).tables;
            if (!tables || tables.length === 0) {
                continue;
            }
            // Only deconstruct sheets if we are sure there are tables to process
            if (!deconstructedSheets) {
                deconstructedSheets = deconstructSheets(convertedSheets);
            }
            for (let table of tables) {
                for (let sheetId in deconstructedSheets) {
                    const sheet = convertedSheets.find((s) => s.id === sheetId);
                    for (let xc in deconstructedSheets[sheetId]) {
                        const deconstructedCell = deconstructedSheets[sheetId][xc];
                        for (let i = deconstructedCell.length - 3; i >= 0; i -= 2) {
                            const possibleTable = deconstructedSheets[sheetId][xc][i];
                            if (!possibleTable.endsWith(table.name)) {
                                continue;
                            }
                            const possibleRef = deconstructedSheets[sheetId][xc][i + 1];
                            const sheetPrefix = tableSheet.id === sheet.id ? "" : tableSheet.name + "!";
                            const convertedRef = convertTableReference(sheetPrefix, possibleRef, table, xc);
                            deconstructedSheets[sheetId][xc][i + 2] =
                                possibleTable.slice(0, possibleTable.indexOf(table.name)) +
                                    convertedRef +
                                    deconstructedSheets[sheetId][xc][i + 2];
                            deconstructedSheets[sheetId][xc].splice(i, 2);
                        }
                    }
                }
            }
        }
        if (!deconstructedSheets) {
            return;
        }
        for (let sheetId in deconstructedSheets) {
            const sheet = convertedSheets.find((s) => s.id === sheetId);
            for (let xc in deconstructedSheets[sheetId]) {
                const deconstructedCell = deconstructedSheets[sheetId][xc];
                if (deconstructedCell.length === 1) {
                    sheet.cells[xc].content = deconstructedCell[0];
                    continue;
                }
                let newContent = "";
                for (let i = 0; i < deconstructedCell.length; i += 2) {
                    newContent += deconstructedCell[i] + "[" + deconstructedCell[i + 1] + "]";
                }
                newContent += deconstructedCell[deconstructedCell.length - 1];
                sheet.cells[xc].content = newContent;
            }
        }
    }
    /**
     * Deconstruct the content of the cells in the sheets to extract possible table references.
     * Example from "=AVERAGE(Table1[colName1])-AVERAGE(Table2[colName2])":
     * return --> ["=AVERAGE(Table1", "colName1", ")-AVERAGE(Table2", "colName2", ")"]
     */
    function deconstructSheets(convertedSheets) {
        var _a;
        const deconstructedSheets = {};
        for (let sheet of convertedSheets) {
            for (let xc in sheet.cells) {
                const cellContent = (_a = sheet.cells[xc]) === null || _a === void 0 ? void 0 : _a.content;
                if (!cellContent || !cellContent.startsWith("=")) {
                    continue;
                }
                const startIndex = cellContent.indexOf("[");
                if (startIndex === -1) {
                    continue;
                }
                const deconstructedCell = [];
                let possibleTable = cellContent.slice(0, startIndex);
                let possibleRef = "";
                let openBrackets = 1;
                let mainPossibleTableIndex = 0;
                let mainOpenBracketIndex = startIndex;
                for (let index = startIndex + 1; index < cellContent.length; index++) {
                    if (cellContent[index] === "[") {
                        if (openBrackets === 0) {
                            possibleTable = cellContent.slice(mainPossibleTableIndex, index);
                            mainOpenBracketIndex = index;
                        }
                        openBrackets++;
                        continue;
                    }
                    if (cellContent[index] === "]") {
                        openBrackets--;
                        if (openBrackets === 0) {
                            possibleRef = cellContent.slice(mainOpenBracketIndex + 1, index);
                            deconstructedCell.push(possibleTable);
                            deconstructedCell.push(possibleRef);
                            mainPossibleTableIndex = index + 1;
                        }
                    }
                }
                if (deconstructedCell.length) {
                    if (!deconstructedSheets[sheet.id]) {
                        deconstructedSheets[sheet.id] = {};
                    }
                    deconstructedCell.push(cellContent.slice(mainPossibleTableIndex));
                    deconstructedSheets[sheet.id][xc] = [...deconstructedCell];
                }
            }
        }
        return deconstructedSheets;
    }
    /**
     * Convert table-specific references in formulas into standard references. A table reference is composed of columns names,
     * and of keywords determining the rows of the table to reference.
     *
     * 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] : reference to the whatever row the keyword refers to
     *  - tableName[[#keyword], [colName]] : reference to some of the element(s) of the column colName
     *  - tableName[[#keyword], [colName]:[col2Name]] : reference to some of the element(s) of the columns colName to col2Name
     *  - tableName[[#keyword1], [#keyword2], [colName]] : reference to all the rows referenced by the keywords in the column colName
     *  - tableName[[#keyword1], [colName], [#keyword2]]: the keywords and colName can be in any order
     *
     *
     * 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
     *
     * Note that the only valid combination of multiple keywords are #Data + #Totals and #Headers + #Data.
     */
    function convertTableReference(sheetPrefix, expr, table, cellXc) {
        // TODO: Ideally we'd want to make a real tokenizer, this simple approach won't work if for example the column name
        // contain # or , characters. But that's probably an edge case that we can ignore for now.
        const parts = expr.split(",").map((part) => part.trim());
        const tableZone = toZone(table.ref);
        const colIndexes = [];
        const rowIndexes = [];
        const foundKeywords = [];
        for (const part of parts) {
            if (removeBrackets(part).startsWith("#")) {
                const keyWord = removeBrackets(part);
                foundKeywords.push(keyWord);
                switch (keyWord) {
                    case "#All":
                        rowIndexes.push(tableZone.top, tableZone.bottom);
                        break;
                    case "#Data":
                        const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
                        const bottom = table.totalsRowCount
                            ? tableZone.bottom - table.totalsRowCount
                            : tableZone.bottom;
                        rowIndexes.push(top, bottom);
                        break;
                    case "#This Row":
                        rowIndexes.push(toCartesian(cellXc).row);
                        break;
                    case "#Headers":
                        if (!table.headerRowCount) {
                            return INCORRECT_RANGE_STRING;
                        }
                        rowIndexes.push(tableZone.top);
                        break;
                    case "#Totals":
                        if (!table.totalsRowCount) {
                            return INCORRECT_RANGE_STRING;
                        }
                        rowIndexes.push(tableZone.bottom);
                        break;
                }
            }
            else {
                const columns = part
                    .split(":")
                    .map((part) => part.trim())
                    .map(removeBrackets);
                if (colIndexes.length) {
                    return INCORRECT_RANGE_STRING;
                }
                const colRelativeIndex = table.cols.findIndex((col) => col.name === columns[0]);
                if (colRelativeIndex === -1) {
                    return INCORRECT_RANGE_STRING;
                }
                colIndexes.push(colRelativeIndex + tableZone.left);
                if (columns[1]) {
                    const colRelativeIndex2 = table.cols.findIndex((col) => col.name === columns[1]);
                    if (colRelativeIndex2 === -1) {
                        return INCORRECT_RANGE_STRING;
                    }
                    colIndexes.push(colRelativeIndex2 + tableZone.left);
                }
            }
        }
        if (!areKeywordsCompatible(foundKeywords)) {
            return INCORRECT_RANGE_STRING;
        }
        if (rowIndexes.length === 0) {
            const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
            const bottom = table.totalsRowCount
                ? tableZone.bottom - table.totalsRowCount
                : tableZone.bottom;
            rowIndexes.push(top, bottom);
        }
        if (colIndexes.length === 0) {
            colIndexes.push(tableZone.left, tableZone.right);
        }
        const refZone = {
            top: Math.min(...rowIndexes),
            left: Math.min(...colIndexes),
            bottom: Math.max(...rowIndexes),
            right: Math.max(...colIndexes),
        };
        return sheetPrefix + zoneToXc(refZone);
    }
    function removeBrackets(str) {
        return str.startsWith("[") && str.endsWith("]") ? str.slice(1, str.length - 1) : str;
    }
    function areKeywordsCompatible(keywords) {
        if (keywords.length < 2) {
            return true;
        }
        else if (keywords.length > 2) {
            return false;
        }
        else if (keywords.includes("#Data") && keywords.includes("#Totals")) {
            return true;
        }
        else if (keywords.includes("#Headers") && keywords.includes("#Data")) {
            return true;
        }
        return false;
    }

    // -------------------------------------
    //            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) {
        const document = new DOMParser().parseFromString(xmlString.toString(), "text/xml");
        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 convertBorderDescr(descr) {
        if (!descr) {
            return undefined;
        }
        return {
            style: descr[0],
            color: { rgb: descr[1] },
        };
    }
    function getDefaultXLSXStructure(data) {
        const xlsxBorders = Object.values(data.borders).map((border) => {
            return {
                left: convertBorderDescr(border.left),
                right: convertBorderDescr(border.right),
                bottom: convertBorderDescr(border.bottom),
                top: convertBorderDescr(border.top),
            };
        });
        const borders = [{}, ...xlsxBorders];
        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: "center" },
                },
            ],
            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 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 namespace of all the xml tags in the string.
     *
     * Eg. : "ns:test a" => "test a"
     */
    function removeNamespaces(query) {
        return query.replace(/[a-z0-9]+:(?=[a-z0-9]+)/gi, "");
    }
    /**
     * Escape the namespace's colons of all the xml tags in the string.
     *
     * Eg. : "ns:test a" => "ns\\:test a"
     */
    function escapeNamespaces(query) {
        return query.replace(/([a-z0-9]+):(?=[a-z0-9]+)/gi, "$1\\:");
    }
    /**
     * Return true if the querySelector ignores the namespaces when searching for a tag in the DOM.
     *
     * Should return true if it's running on a browser, and false if it's running on jest (jsdom).
     */
    function areNamespaceIgnoredByQuerySelector() {
        const doc = new DOMParser().parseFromString("<t:test xmlns:t='a'/>", "text/xml");
        return doc.querySelector("test") !== null;
    }

    class AttributeValue {
        constructor(value) {
            this.value = value;
        }
        asString() {
            return fixXlsxUnicode(String(this.value));
        }
        asBool() {
            return Boolean(Number(this.value));
        }
        asNum() {
            return Number(this.value);
        }
    }
    class XlsxBaseExtractor {
        constructor(rootFile, xlsxStructure, warningManager) {
            // 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
            this.currentFile = undefined;
            this.rootFile = rootFile;
            this.currentFile = rootFile.file.fileName;
            this.xlsxFileStructure = xlsxStructure;
            this.warningManager = warningManager;
            this.areNamespaceIgnored = areNamespaceIgnoredByQuerySelector();
            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
         */
        getListOfFiles() {
            const files = Object.values(this.xlsxFileStructure).flat().filter(isDefined$1);
            return files;
        }
        /**
         * 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) {
            var _a;
            const ret = [];
            const oldWorkingDocument = this.currentFile;
            let elements;
            if (args.children) {
                const children = (_a = this.querySelector(args.parent, args.query)) === null || _a === void 0 ? void 0 : _a.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 === null || attribute === void 0 ? void 0 : attribute.value) ? attribute.value : optionalArgs === null || optionalArgs === void 0 ? void 0 : 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) {
            var _a;
            if ((optionalArgs === null || optionalArgs === void 0 ? void 0 : optionalArgs.default) !== undefined && typeof optionalArgs.default !== "string") {
                throw new Error("extractTextContent default value should be a string");
            }
            const shouldPreserveSpaces = ((_a = element === null || element === void 0 ? void 0 : element.attributes["xml:space"]) === null || _a === void 0 ? void 0 : _a.value) === "preserve";
            let textContent = element === null || element === void 0 ? void 0 : element.textContent;
            if (!element || textContent === null) {
                this.handleMissingValue(element, `text content`, optionalArgs);
            }
            if (textContent) {
                textContent = shouldPreserveSpaces ? textContent : textContent.trim();
            }
            return (textContent ? fixXlsxUnicode(textContent) : optionalArgs === null || optionalArgs === void 0 ? void 0 : 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) {
            var _a;
            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
                ? (_a = this.extractAttr(child, attName, optionalArgs)) === null || _a === void 0 ? void 0 : _a.asString()
                : optionalArgs === null || optionalArgs === void 0 ? void 0 : 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 === null || optionalArgs === void 0 ? void 0 : 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 === null || optionalArgs === void 0 ? void 0 : 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 === null || optionalArgs === void 0 ? void 0 : optionalArgs.required) {
                if ((optionalArgs === null || optionalArgs === void 0 ? void 0 : optionalArgs.default) !== undefined) {
                    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) {
            var _a, _b, _c, _d, _e;
            if (!colorElement) {
                return defaultColor ? { rgb: defaultColor } : undefined;
            }
            const themeIndex = (_a = this.extractAttr(colorElement, "theme")) === null || _a === void 0 ? void 0 : _a.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 = (_b = this.extractAttr(colorElement, "rgb")) === null || _b === void 0 ? void 0 : _b.asString();
            }
            const color = {
                rgb,
                auto: (_c = this.extractAttr(colorElement, "auto")) === null || _c === void 0 ? void 0 : _c.asBool(),
                indexed: (_d = this.extractAttr(colorElement, "indexed")) === null || _d === void 0 ? void 0 : _d.asNum(),
                tint: (_e = this.extractAttr(colorElement, "tint")) === null || _e === void 0 ? void 0 : _e.asNum(),
            };
            return color;
        }
        /**
         * Returns the xlsx file targeted by a relationship.
         */
        getTargetXmlFile(relationship) {
            if (!relationship)
                throw new Error("Undefined target file");
            let target = relationship.target;
            target = target.replace("../", "");
            target = target.replace("./", "");
            // Use "endsWith" because targets are relative paths, and we know the files by their absolute path.
            const f = this.getListOfFiles().find((f) => f.file.fileName.endsWith(target));
            if (!f || !f.file)
                throw new Error("Cannot find target file");
            return f;
        }
        /**
         * Wrapper of querySelector, but we'll remove the namespaces from the query if areNamespacesIgnored is true.
         *
         * Why we need to do this :
         *  - For an XML "<t:test />"
         *  - on Jest(jsdom) : xml.querySelector("test") == null, xml.querySelector("t\\:test") == <t:test />
         *  - on Browser : xml.querySelector("test") == <t:test />, xml.querySelector("t\\:test") == null
         */
        querySelector(element, query) {
            query = this.areNamespaceIgnored ? removeNamespaces(query) : escapeNamespaces(query);
            return element.querySelector(query);
        }
        /**
         * Wrapper of querySelectorAll, but we'll remove the namespaces from the query if areNamespacesIgnored is true.
         *
         * Why we need to do this :
         *  - For an XML "<t:test />"
         *  - on Jest(jsdom) : xml.querySelectorAll("test") == [], xml.querySelectorAll("t\\:test") == [<t:test />]
         *  - on Browser : xml.querySelectorAll("test") == [<t:test />], xml.querySelectorAll("t\\:test") == []
         */
        querySelectorAll(element, query) {
            query = this.areNamespaceIgnored ? removeNamespaces(query) : escapeNamespaces(query);
            return element.querySelectorAll(query);
        }
        /**
         * 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;
            }
        }
    }

    /**
     * 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 {
        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) => {
                var _a;
                return {
                    // sqref = ranges on which the cf applies, separated by spaces
                    sqref: this.extractAttr(cfElement, "sqref", { required: true }).asString().split(" "),
                    pivot: (_a = this.extractAttr(cfElement, "pivot")) === null || _a === void 0 ? void 0 : _a.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) => {
                var _a;
                return {
                    sqref: this.extractChildTextContent(cfElement, "xm:sqref", { required: true }).split(" "),
                    pivot: (_a = this.extractAttr(cfElement, "xm:pivot")) === null || _a === void 0 ? void 0 : _a.asBool(),
                    cfRules: this.extractCFRules(cfElement, this.theme),
                };
            }));
            return cfs;
        }
        extractCFRules(cfElement, theme) {
            return this.mapOnElements({ parent: cfElement, query: "cfRule, x14:cfRule" }, (cfRuleElement) => {
                var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
                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: (_a = this.extractAttr(cfRuleElement, "dxfId")) === null || _a === void 0 ? void 0 : _a.asNum(),
                    stopIfTrue: (_b = this.extractAttr(cfRuleElement, "stopIfTrue")) === null || _b === void 0 ? void 0 : _b.asBool(),
                    aboveAverage: (_c = this.extractAttr(cfRuleElement, "aboveAverage")) === null || _c === void 0 ? void 0 : _c.asBool(),
                    percent: (_d = this.extractAttr(cfRuleElement, "percent")) === null || _d === void 0 ? void 0 : _d.asBool(),
                    bottom: (_e = this.extractAttr(cfRuleElement, "bottom")) === null || _e === void 0 ? void 0 : _e.asBool(),
                    operator: (_f = this.extractAttr(cfRuleElement, "operator")) === null || _f === void 0 ? void 0 : _f.asString(),
                    text: (_g = this.extractAttr(cfRuleElement, "text")) === null || _g === void 0 ? void 0 : _g.asString(),
                    timePeriod: (_h = this.extractAttr(cfRuleElement, "timePeriod")) === null || _h === void 0 ? void 0 : _h.asString(),
                    rank: (_j = this.extractAttr(cfRuleElement, "rank")) === null || _j === void 0 ? void 0 : _j.asNum(),
                    stdDev: (_k = this.extractAttr(cfRuleElement, "stdDev")) === null || _k === void 0 ? void 0 : _k.asNum(),
                    equalAverage: (_l = this.extractAttr(cfRuleElement, "equalAverage")) === null || _l === void 0 ? void 0 : _l.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) {
            var _a, _b;
            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: (_a = this.extractAttr(iconSetElement, "reverse")) === null || _a === void 0 ? void 0 : _a.asBool(),
                custom: (_b = this.extractAttr(iconSetElement, "custom")) === null || _b === void 0 ? void 0 : _b.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) => {
                var _a, _b;
                return {
                    type: this.extractAttr(cfVoElement, "type", {
                        required: true,
                    }).asString(),
                    gte: (_a = this.extractAttr(cfVoElement, "gte", { default: true })) === null || _a === void 0 ? void 0 : _a.asBool(),
                    value: cfVoElement.attributes["val"]
                        ? (_b = this.extractAttr(cfVoElement, "val")) === null || _b === void 0 ? void 0 : _b.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}`)),
                    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: "none",
                    }).asString()],
                    stacked: barChartGrouping === "stacked",
                    fontColor: "000000",
                };
            })[0];
        }
        extractChartDatasets(chartElement) {
            return this.mapOnElements({ parent: chartElement, query: "c:ser" }, (chartDataElement) => {
                return {
                    label: this.extractChildTextContent(chartDataElement, "c:cat 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 = removeNamespaces(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 = removeNamespaces(figureElement.tagName);
                if (anchorType !== "twoCellAnchor") {
                    throw new Error("Only twoCellAnchor are supported for xlsx drawings.");
                }
                const chartElement = this.querySelector(figureElement, "c:chart");
                if (!chartElement) {
                    throw new Error("Only chart figures are currently supported.");
                }
                return {
                    anchors: [
                        this.extractFigureAnchor("xdr:from", figureElement),
                        this.extractFigureAnchor("xdr:to", figureElement),
                    ],
                    data: this.extractChart(chartElement),
                };
            });
        }
        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;
        }
    }

    /**
     * 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) => {
                var _a;
                return {
                    displayName: this.extractAttr(tableElement, "displayName", {
                        required: true,
                    }).asString(),
                    name: (_a = this.extractAttr(tableElement, "name")) === null || _a === void 0 ? void 0 : _a.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) => {
                var _a, _b, _c, _d, _e;
                return {
                    name: (_a = this.extractAttr(tableStyleElement, "name")) === null || _a === void 0 ? void 0 : _a.asString(),
                    showFirstColumn: (_b = this.extractAttr(tableStyleElement, "showFirstColumn")) === null || _b === void 0 ? void 0 : _b.asBool(),
                    showLastColumn: (_c = this.extractAttr(tableStyleElement, "showLastColumn")) === null || _c === void 0 ? void 0 : _c.asBool(),
                    showRowStripes: (_d = this.extractAttr(tableStyleElement, "showRowStripes")) === null || _d === void 0 ? void 0 : _d.asBool(),
                    showColumnStripes: (_e = this.extractAttr(tableStyleElement, "showColumnStripes")) === null || _e === void 0 ? void 0 : _e.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 {
        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) => {
                var _a;
                const drawingId = (_a = this.extractAttr(drawingElement, "r:id", { required: true })) === null || _a === void 0 ? void 0 : _a.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) => {
                var _a;
                const tableId = (_a = this.extractAttr(tablePartElement, "r:id", { required: true })) === null || _a === void 0 ? void 0 : _a.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) => {
                var _a, _b, _c, _d, _e, _f, _g;
                return {
                    width: (_a = this.extractAttr(colElement, "width")) === null || _a === void 0 ? void 0 : _a.asNum(),
                    customWidth: (_b = this.extractAttr(colElement, "customWidth")) === null || _b === void 0 ? void 0 : _b.asBool(),
                    bestFit: (_c = this.extractAttr(colElement, "bestFit")) === null || _c === void 0 ? void 0 : _c.asBool(),
                    hidden: (_d = this.extractAttr(colElement, "hidden")) === null || _d === void 0 ? void 0 : _d.asBool(),
                    min: (_e = this.extractAttr(colElement, "min", { required: true })) === null || _e === void 0 ? void 0 : _e.asNum(),
                    max: (_f = this.extractAttr(colElement, "max", { required: true })) === null || _f === void 0 ? void 0 : _f.asNum(),
                    styleIndex: (_g = this.extractAttr(colElement, "style")) === null || _g === void 0 ? void 0 : _g.asNum(),
                };
            });
        }
        extractRows(worksheet) {
            return this.mapOnElements({ parent: worksheet, query: "sheetData row" }, (rowElement) => {
                var _a, _b, _c, _d, _e;
                return {
                    index: (_a = this.extractAttr(rowElement, "r", { required: true })) === null || _a === void 0 ? void 0 : _a.asNum(),
                    cells: this.extractCells(rowElement),
                    height: (_b = this.extractAttr(rowElement, "ht")) === null || _b === void 0 ? void 0 : _b.asNum(),
                    customHeight: (_c = this.extractAttr(rowElement, "customHeight")) === null || _c === void 0 ? void 0 : _c.asBool(),
                    hidden: (_d = this.extractAttr(rowElement, "hidden")) === null || _d === void 0 ? void 0 : _d.asBool(),
                    styleIndex: (_e = this.extractAttr(rowElement, "s")) === null || _e === void 0 ? void 0 : _e.asNum(),
                };
            });
        }
        extractCells(row) {
            return this.mapOnElements({ parent: row, query: "c" }, (cellElement) => {
                var _a, _b, _c;
                return {
                    xc: (_a = this.extractAttr(cellElement, "r", { required: true })) === null || _a === void 0 ? void 0 : _a.asString(),
                    styleIndex: (_b = this.extractAttr(cellElement, "s")) === null || _b === void 0 ? void 0 : _b.asNum(),
                    type: CELL_TYPE_CONVERSION_MAP[(_c = this.extractAttr(cellElement, "t", { default: "n" })) === null || _c === void 0 ? void 0 : _c.asString()],
                    value: this.extractChildTextContent(cellElement, "v"),
                    formula: this.extractCellFormula(cellElement),
                };
            });
        }
        extractCellFormula(cellElement) {
            var _a, _b;
            const formulaElement = this.querySelector(cellElement, "f");
            if (!formulaElement)
                return undefined;
            return {
                content: this.extractTextContent(formulaElement),
                sharedIndex: (_a = this.extractAttr(formulaElement, "si")) === null || _a === void 0 ? void 0 : _a.asNum(),
                ref: (_b = this.extractAttr(formulaElement, "ref")) === null || _b === void 0 ? void 0 : _b.asString(),
            };
        }
        extractHyperLinks(worksheet) {
            return this.mapOnElements({ parent: worksheet, query: "hyperlink" }, (linkElement) => {
                var _a, _b, _c, _d;
                const relId = (_a = this.extractAttr(linkElement, "r:id")) === null || _a === void 0 ? void 0 : _a.asString();
                return {
                    xc: (_b = this.extractAttr(linkElement, "ref", { required: true })) === null || _b === void 0 ? void 0 : _b.asString(),
                    location: (_c = this.extractAttr(linkElement, "location")) === null || _c === void 0 ? void 0 : _c.asString(),
                    display: (_d = this.extractAttr(linkElement, "display")) === null || _d === void 0 ? void 0 : _d.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 {
        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) {
            var _a, _b, _c, _d;
            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 && ((_a = italicElement.attributes["val"]) === null || _a === void 0 ? void 0 : _a.value) !== "0";
            const boldElement = this.querySelector(fontElement, `b`) || undefined;
            const bold = boldElement && ((_b = boldElement.attributes["val"]) === null || _b === void 0 ? void 0 : _b.value) !== "0";
            const strikeElement = this.querySelector(fontElement, `strike`) || undefined;
            const strike = strikeElement && ((_c = strikeElement.attributes["val"]) === null || _c === void 0 ? void 0 : _c.value) !== "0";
            const underlineElement = this.querySelector(fontElement, `u`) || undefined;
            const underline = underlineElement && ((_d = underlineElement.attributes["val"]) === null || _d === void 0 ? void 0 : _d.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) {
            var _a;
            // Fills are either patterns of gradients
            const fillChild = fillElement.children[0];
            if (fillChild.tagName === "patternFill") {
                return {
                    patternType: (_a = fillChild.attributes["patternType"]) === null || _a === void 0 ? void 0 : _a.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) {
            var _a, _b;
            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 = (_a = this.extractAttr(borderElement, "diagonalUp")) === null || _a === void 0 ? void 0 : _a.asBool();
                border.diagonalDown = (_b = this.extractAttr(borderElement, "diagonalDown")) === null || _b === void 0 ? void 0 : _b.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) {
            var _a, _b, _c, _d, _e, _f, _g;
            return {
                horizontal: this.extractAttr(alignmentElement, "horizontal", {
                    default: "general",
                }).asString(),
                vertical: this.extractAttr(alignmentElement, "vertical", {
                    default: "center",
                }).asString(),
                textRotation: (_a = this.extractAttr(alignmentElement, "textRotation")) === null || _a === void 0 ? void 0 : _a.asNum(),
                wrapText: (_b = this.extractAttr(alignmentElement, "wrapText")) === null || _b === void 0 ? void 0 : _b.asBool(),
                indent: (_c = this.extractAttr(alignmentElement, "indent")) === null || _c === void 0 ? void 0 : _c.asNum(),
                relativeIndent: (_d = this.extractAttr(alignmentElement, "relativeIndent")) === null || _d === void 0 ? void 0 : _d.asNum(),
                justifyLastLine: (_e = this.extractAttr(alignmentElement, "justifyLastLine")) === null || _e === void 0 ? void 0 : _e.asBool(),
                shrinkToFit: (_f = this.extractAttr(alignmentElement, "shrinkToFit")) === null || _f === void 0 ? void 0 : _f.asBool(),
                readingOrder: (_g = this.extractAttr(alignmentElement, "readingOrder")) === null || _g === void 0 ? void 0 : _g.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 === null || item === void 0 ? void 0 : 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 {
        constructor(files) {
            this.warningManager = new XLSXImportWarningManager();
            this.xmls = {};
            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")) {
                    this.xmls[key] = parseXML(new XMLString(files[key]));
                }
            }
        }
        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),
            };
            if (!xlsxFileStructure.workbook.rels) {
                throw Error(_lt("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;
        }
    }

    /**
     * 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 = 12.5;
    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);
                }
            }
        }
        data = deepCopy(data);
        // 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) {
                var _a;
                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 (((_a = cf.rule[thresholdName]) === null || _a === void 0 ? void 0 : _a.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;
            },
        },
    ];
    /**
     * 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.smallUuid();
                }
                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(data) {
        data = Object.assign(createEmptyWorkbookData(), data, { 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"));
        }
        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);
        initialMessages = fixTranslatedDuplicateSheetName(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) {
        var _a;
        const messages = [];
        const map = {};
        for (const sheet of data.sheets || []) {
            (_a = sheet.figures) === null || _a === void 0 ? void 0 : _a.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;
    }
    function fixTranslatedDuplicateSheetName(data, initialMessages) {
        var _a;
        const sheetNames = {};
        for (const sheet of data.sheets || []) {
            sheetNames[sheet.id] = sheet.name;
        }
        const messages = [];
        for (const message of initialMessages) {
            if (message.type === "REMOTE_REVISION") {
                const commands = [];
                for (const cmd of message.commands) {
                    switch (cmd.type) {
                        case "DUPLICATE_SHEET":
                            cmd.sheetNameTo =
                                (_a = cmd.sheetNameTo) !== null && _a !== void 0 ? _a : getDuplicateSheetName(sheetNames[cmd.sheetId], Object.values(sheetNames));
                            break;
                        case "CREATE_SHEET":
                        case "RENAME_SHEET":
                            sheetNames[cmd.sheetId] = cmd.name || getNextSheetName(Object.values(sheetNames));
                            break;
                    }
                    commands.push(cmd);
                }
                messages.push({
                    ...message,
                    commands,
                });
            }
            else {
                messages.push(message);
            }
        }
        return initialMessages;
    }
    // -----------------------------------------------------------------------------
    // 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,
        };
        return data;
    }
    function createEmptyExcelSheet(sheetId, name) {
        return {
            ...createEmptySheet(sheetId, name),
            charts: [],
        };
    }
    function createEmptyExcelWorkbookData() {
        return {
            ...createEmptyWorkbookData(),
            sheets: [createEmptyExcelSheet(INITIAL_SHEET_ID, "Sheet1")],
        };
    }

    /**
     * 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 {
        constructor(getters, stateObserver, range, dispatch, config, uuidGenerator) {
            super(stateObserver, dispatch, config);
            this.dispatch = dispatch;
            this.range = range;
            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) { }
    }

    /**
     * Formatting plugin.
     *
     * This plugin manages all things related to a cell look:
     * - borders
     */
    class BordersPlugin extends CorePlugin {
        constructor() {
            super(...arguments);
            this.borders = {};
        }
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "SET_BORDER":
                    return this.checkBordersUnchanged(cmd);
                default:
                    return 0 /* 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 === null || col === void 0 ? void 0 : 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_FORMATTING":
                    if (cmd.border) {
                        const target = cmd.target.map((zone) => this.getters.expandZone(cmd.sheetId, zone));
                        this.setBorders(cmd.sheetId, target, cmd.border);
                    }
                    break;
                case "CLEAR_FORMATTING":
                    this.clearBorders(cmd.sheetId, cmd.target);
                    break;
                case "REMOVE_COLUMNS_ROWS":
                    const elements = [...cmd.elements].sort((a, b) => b - a);
                    for (const group of groupConsecutive(elements)) {
                        if (cmd.dimension === "COL") {
                            if (group[0] >= this.getters.getNumberCols(cmd.sheetId)) {
                                for (let row = 0; row < this.getters.getNumberRows(cmd.sheetId); row++) {
                                    this.history.update("borders", cmd.sheetId, group[0] + 1, row, "vertical", undefined);
                                }
                            }
                            if (group[group.length - 1] === 0) {
                                for (let row = 0; row < this.getters.getNumberRows(cmd.sheetId); row++) {
                                    this.history.update("borders", cmd.sheetId, 0, row, "vertical", undefined);
                                }
                            }
                            const zone = this.getters.getColsZone(cmd.sheetId, group[group.length - 1] + 1, group[0]);
                            this.clearInsideBorders(cmd.sheetId, [zone]);
                            this.shiftBordersHorizontally(cmd.sheetId, group[0] + 1, -group.length);
                        }
                        else {
                            if (group[0] >= this.getters.getNumberRows(cmd.sheetId)) {
                                for (let col = 0; col < this.getters.getNumberCols(cmd.sheetId); col++) {
                                    this.history.update("borders", cmd.sheetId, col, group[0] + 1, "horizontal", undefined);
                                }
                            }
                            if (group[group.length - 1] === 0) {
                                for (let col = 0; col < this.getters.getNumberCols(cmd.sheetId); col++) {
                                    this.history.update("borders", cmd.sheetId, col, 0, "horizontal", undefined);
                                }
                            }
                            const zone = this.getters.getRowsZone(cmd.sheetId, group[group.length - 1] + 1, group[0]);
                            this.clearInsideBorders(cmd.sheetId, [zone]);
                            this.shiftBordersVertically(cmd.sheetId, group[0] + 1, -group.length);
                        }
                    }
                    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) {
            var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
            const border = {
                top: (_c = (_b = (_a = this.borders[sheetId]) === null || _a === void 0 ? void 0 : _a[col]) === null || _b === void 0 ? void 0 : _b[row]) === null || _c === void 0 ? void 0 : _c.horizontal,
                bottom: (_f = (_e = (_d = this.borders[sheetId]) === null || _d === void 0 ? void 0 : _d[col]) === null || _e === void 0 ? void 0 : _e[row + 1]) === null || _f === void 0 ? void 0 : _f.horizontal,
                left: (_j = (_h = (_g = this.borders[sheetId]) === null || _g === void 0 ? void 0 : _g[col]) === null || _h === void 0 ? void 0 : _h[row]) === null || _j === void 0 ? void 0 : _j.vertical,
                right: (_m = (_l = (_k = this.borders[sheetId]) === null || _k === void 0 ? void 0 : _k[col + 1]) === null || _l === void 0 ? void 0 : _l[row]) === null || _m === void 0 ? void 0 : _m.vertical,
            };
            if (!border.bottom && !border.left && !border.right && !border.top) {
                return null;
            }
            return border;
        }
        // ---------------------------------------------------------------------------
        // 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, leftColumn, row);
                const rightBorder = this.getCellBorder(sheetId, 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, topRow);
                const belowBorder = this.getCellBorder(sheetId, col, 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 all the rows which contains at least a border
         */
        getRowsWithBorders(sheetId) {
            var _a;
            const sheetBorders = (_a = this.borders[sheetId]) === null || _a === void 0 ? void 0 : _a.filter(isDefined$1);
            if (!sheetBorders)
                return [];
            const rowsWithBorders = new Set();
            for (const rowBorders of sheetBorders) {
                for (const rowBorder in rowBorders) {
                    rowsWithBorders.add(parseInt(rowBorder, 10));
                }
            }
            return Array.from(rowsWithBorders);
        }
        /**
         * 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.getRowsWithBorders(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) => {
                var _a, _b, _c, _d;
                const targetBorder = (_b = (_a = borders[col]) === null || _a === void 0 ? void 0 : _a[row + delta]) === null || _b === void 0 ? void 0 : _b[borderDirection];
                const movedBorder = (_d = (_c = borders[col]) === null || _c === void 0 ? void 0 : _c[row]) === null || _d === void 0 ? void 0 : _d[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) => {
                var _a, _b, _c, _d;
                const targetBorder = (_b = (_a = borders[col + delta]) === null || _a === void 0 ? void 0 : _a[row]) === null || _b === void 0 ? void 0 : _b[borderDirection];
                const movedBorder = (_d = (_c = borders[col]) === null || _c === void 0 ? void 0 : _c[row]) === null || _d === void 0 ? void 0 : _d[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) {
            var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
            if (override || !((_d = (_c = (_b = (_a = this.borders) === null || _a === void 0 ? void 0 : _a[sheetId]) === null || _b === void 0 ? void 0 : _b[col]) === null || _c === void 0 ? void 0 : _c[row]) === null || _d === void 0 ? void 0 : _d.vertical)) {
                this.history.update("borders", sheetId, col, row, "vertical", border === null || border === void 0 ? void 0 : border.left);
            }
            if (override || !((_h = (_g = (_f = (_e = this.borders) === null || _e === void 0 ? void 0 : _e[sheetId]) === null || _f === void 0 ? void 0 : _f[col]) === null || _g === void 0 ? void 0 : _g[row]) === null || _h === void 0 ? void 0 : _h.horizontal)) {
                this.history.update("borders", sheetId, col, row, "horizontal", border === null || border === void 0 ? void 0 : border.top);
            }
            if (override || !((_m = (_l = (_k = (_j = this.borders) === null || _j === void 0 ? void 0 : _j[sheetId]) === null || _k === void 0 ? void 0 : _k[col + 1]) === null || _l === void 0 ? void 0 : _l[row]) === null || _m === void 0 ? void 0 : _m.vertical)) {
                this.history.update("borders", sheetId, col + 1, row, "vertical", border === null || border === void 0 ? void 0 : border.right);
            }
            if (override || !((_r = (_q = (_p = (_o = this.borders) === null || _o === void 0 ? void 0 : _o[sheetId]) === null || _p === void 0 ? void 0 : _p[col]) === null || _q === void 0 ? void 0 : _q[row + 1]) === null || _r === void 0 ? void 0 : _r.horizontal)) {
                this.history.update("borders", sheetId, col, row + 1, "horizontal", border === null || border === void 0 ? void 0 : 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);
                }
            }
        }
        /**
         * Remove the borders inside of a zone
         */
        clearInsideBorders(sheetId, zones) {
            for (let zone of zones) {
                for (let row = zone.top; row <= zone.bottom; row++) {
                    for (let col = zone.left; col <= zone.right; col++) {
                        this.history.update("borders", sheetId, col, row, 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, command) {
            if (command === "clear") {
                return this.clearBorders(sheetId, zones);
            }
            for (let zone of zones) {
                if (command === "h" || command === "hv" || command === "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: DEFAULT_BORDER_DESC });
                        }
                    }
                }
                if (command === "v" || command === "hv" || command === "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: DEFAULT_BORDER_DESC });
                        }
                    }
                }
                if (command === "left" || command === "all" || command === "external") {
                    for (let row = zone.top; row <= zone.bottom; row++) {
                        this.addBorder(sheetId, zone.left, row, { left: DEFAULT_BORDER_DESC });
                    }
                }
                if (command === "right" || command === "all" || command === "external") {
                    for (let row = zone.top; row <= zone.bottom; row++) {
                        this.addBorder(sheetId, zone.right + 1, row, { left: DEFAULT_BORDER_DESC });
                    }
                }
                if (command === "top" || command === "all" || command === "external") {
                    for (let col = zone.left; col <= zone.right; col++) {
                        this.addBorder(sheetId, col, zone.top, { top: DEFAULT_BORDER_DESC });
                    }
                }
                if (command === "bottom" || command === "all" || command === "external") {
                    for (let col = zone.left; col <= zone.right; col++) {
                        this.addBorder(sheetId, col, zone.bottom + 1, { top: DEFAULT_BORDER_DESC });
                    }
                }
            }
        }
        /**
         * Compute the borders to add to the given zone merged.
         */
        addBordersToMerge(sheetId, zone) {
            const { left, right, top, bottom } = zone;
            const bordersTopLeft = this.getCellBorder(sheetId, left, top);
            const bordersBottomRight = this.getCellBorder(sheetId, right, bottom);
            this.clearBorders(sheetId, [zone]);
            if (bordersTopLeft === null || bordersTopLeft === void 0 ? void 0 : bordersTopLeft.top) {
                this.setBorders(sheetId, [{ ...zone, bottom: top }], "top");
            }
            if (bordersTopLeft === null || bordersTopLeft === void 0 ? void 0 : bordersTopLeft.left) {
                this.setBorders(sheetId, [{ ...zone, right: left }], "left");
            }
            if ((bordersBottomRight === null || bordersBottomRight === void 0 ? void 0 : bordersBottomRight.bottom) || (bordersTopLeft === null || bordersTopLeft === void 0 ? void 0 : bordersTopLeft.bottom)) {
                this.setBorders(sheetId, [{ ...zone, top: bottom }], "bottom");
            }
            if ((bordersBottomRight === null || bordersBottomRight === void 0 ? void 0 : bordersBottomRight.right) || (bordersTopLeft === null || bordersTopLeft === void 0 ? void 0 : bordersTopLeft.right)) {
                this.setBorders(sheetId, [{ ...zone, left: right }], "right");
            }
        }
        checkBordersUnchanged(cmd) {
            var _a, _b, _c, _d;
            const currentBorder = this.getCellBorder(cmd.sheetId, cmd.col, cmd.row);
            const areAllNewBordersUndefined = !((_a = cmd.border) === null || _a === void 0 ? void 0 : _a.bottom) && !((_b = cmd.border) === null || _b === void 0 ? void 0 : _b.left) && !((_c = cmd.border) === null || _c === void 0 ? void 0 : _c.right) && !((_d = cmd.border) === null || _d === void 0 ? void 0 : _d.top);
            if ((!currentBorder && areAllNewBordersUndefined) || deepEquals(currentBorder, cmd.border)) {
                return 89 /* CommandResult.NoChanges */;
            }
            return 0 /* CommandResult.Success */;
        }
        // ---------------------------------------------------------------------------
        // Import/Export
        // ---------------------------------------------------------------------------
        import(data) {
            // Borders
            if (data.borders) {
                for (let sheet of data.sheets) {
                    for (let [xc, cell] of Object.entries(sheet.cells)) {
                        if (cell === null || cell === void 0 ? void 0 : 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) {
            // Borders
            let borderId = 0;
            const borders = {};
            /**
             * Get the id of the given border. If the border does not exist, it creates
             * one.
             */
            function getBorderId(border) {
                for (let [key, value] of Object.entries(borders)) {
                    if (deepEquals(value, border)) {
                        return parseInt(key, 10);
                    }
                }
                borders[++borderId] = border;
                return borderId;
            }
            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(sheet.id, col, row);
                        if (border) {
                            const xc = toXC(col, row);
                            const cell = sheet.cells[xc];
                            const borderId = getBorderId(border);
                            if (cell) {
                                cell.border = borderId;
                            }
                            else {
                                sheet.cells[xc] = { border: borderId };
                            }
                        }
                    }
                }
            }
            data.borders = borders;
        }
        exportForExcel(data) {
            this.export(data);
        }
    }
    BordersPlugin.getters = ["getCellBorder"];

    const LINK_STYLE = { textColor: LINK_COLOR };
    /**
     * Core Plugin
     *
     * This is the most fundamental of all plugins. It defines how to interact with
     * cell and sheet content.
     */
    class CellPlugin extends CorePlugin {
        constructor() {
            super(...arguments);
            this.nextId = 1;
            this.cells = {};
            this.createCell = cellFactory(this.getters);
        }
        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.dependencies) {
                            if (!sheetId || range.sheetId === sheetId) {
                                const change = applyChange(range);
                                if (change.changeType !== "NONE") {
                                    this.history.update("cells", sheet, cell.id, "dependencies", cell.dependencies.indexOf(range), change.range);
                                }
                            }
                        }
                    }
                }
            }
        }
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        allowDispatch(cmd) {
            var _a;
            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);
                case "UPDATE_CELL_POSITION":
                    return !cmd.cellId || ((_a = this.cells[cmd.sheetId]) === null || _a === void 0 ? void 0 : _a[cmd.cellId])
                        ? 0 /* CommandResult.Success */
                        : 28 /* CommandResult.InvalidCellId */;
                default:
                    return 0 /* 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;
                case "DELETE_SHEET": {
                    this.history.update("cells", cmd.sheetId, undefined);
                }
            }
        }
        /**
         * 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 === null || cellData === void 0 ? void 0 : cellData.content) || (cellData === null || cellData === void 0 ? void 0 : cellData.format) || (cellData === null || cellData === void 0 ? void 0 : 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 { col, row } of positions) {
                    const cell = this.getters.getCell(_sheet.id, col, row);
                    const xc = toXC(col, row);
                    cells[xc] = {
                        style: cell.style ? getItemId(cell.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();
            const properties = { format, style };
            return this.createCell(cellId, (cellData === null || cellData === void 0 ? void 0 : cellData.content) || "", properties, sheetId);
        }
        exportForExcel(data) {
            this.export(data);
            for (let sheet of data.sheets) {
                for (const xc in sheet.cells) {
                    const { col, row } = toCartesian(xc);
                    const cell = this.getters.getCell(sheet.id, col, row);
                    const exportedCellData = sheet.cells[xc];
                    exportedCellData.value = cell.evaluated.value;
                    exportedCellData.isFormula = cell.isFormula();
                    if (cell.isFormula()) {
                        exportedCellData.content = this.buildFormulaContent(sheet.id, cell, cell.dependencies, true);
                    }
                    if (cell.format !== cell.evaluated.format) {
                        exportedCellData.computedFormat = cell.evaluated.format;
                    }
                }
            }
        }
        // ---------------------------------------------------------------------------
        // 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
         */
        buildFormulaContent(sheetId, cell, dependencies, useFixedZone = false) {
            const ranges = dependencies || cell.dependencies;
            let rangeIndex = 0;
            return concat(cell.compiledFormula.tokens.map((token) => {
                if (token.type === "REFERENCE") {
                    const range = ranges[rangeIndex++];
                    return this.getters.getRangeString(range, sheetId, useFixedZone);
                }
                return token.value;
            }));
        }
        getFormulaCellContent(sheetId, cell) {
            return this.buildFormulaContent(sheetId, cell);
        }
        getCellStyle(cell) {
            if (!cell) {
                return {};
            }
            const linkStyle = cell.isLink() ? LINK_STYLE : {};
            return { ...linkStyle, ...cell.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, zone.left, zone.top);
            const cellBotRight = this.getters.getMainCellPosition(sheetId, zone.right, 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 === null || cell === void 0 ? void 0 : 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 { col: mainCol, row: mainRow } = this.getters.getMainCellPosition(sheetId, col, row);
            const cell = this.getters.getCell(sheetId, mainCol, mainRow);
            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 ? after.content : before === null || before === void 0 ? void 0 : before.content) || "";
            let style;
            if (after.style !== undefined) {
                style = after.style || undefined;
            }
            else {
                style = before ? before.style : undefined;
            }
            let format = ("format" in after ? after.format : before && before.format) || NULL_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.isEmpty()))) &&
                !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 === null || before === void 0 ? void 0 : before.id) || this.getNextUid();
            const didContentChange = hasContent;
            const properties = { format, style };
            const cell = this.createCell(cellId, afterContent, properties, sheetId);
            if (before && !didContentChange && cell.isFormula()) {
                // content is not re-evaluated if the content did not change => reassign the value manually
                // TODO this plugin should not care about evaluation
                // and evaluation should not depend on implementation details here.
                // Task 2813749
                cell.assignEvaluation(lazy(before.evaluated));
            }
            this.history.update("cells", sheetId, cell.id, cell);
            this.dispatch("UPDATE_CELL_POSITION", { cellId: cell.id, col, row, sheetId });
        }
        checkCellOutOfSheet(cmd) {
            const { sheetId, col, row } = cmd;
            const sheet = this.getters.tryGetSheet(sheetId);
            if (!sheet)
                return 27 /* CommandResult.InvalidSheetId */;
            const sheetZone = this.getters.getSheetZone(sheetId);
            return isInside(col, row, sheetZone) ? 0 /* CommandResult.Success */ : 18 /* CommandResult.TargetOutOfSheet */;
        }
        checkUselessClearCell(cmd) {
            const cell = this.getters.getCell(cmd.sheetId, cmd.col, cmd.row);
            if (!cell)
                return 89 /* CommandResult.NoChanges */;
            if (!cell.content && !cell.style && !cell.format) {
                return 89 /* CommandResult.NoChanges */;
            }
            return 0 /* CommandResult.Success */;
        }
        checkUselessUpdateCell(cmd) {
            const cell = this.getters.getCell(cmd.sheetId, cmd.col, cmd.row);
            const hasContent = "content" in cmd || "formula" in cmd;
            const hasStyle = "style" in cmd;
            const hasFormat = "format" in cmd;
            if ((!hasContent || (cell === null || cell === void 0 ? void 0 : cell.content) === cmd.content) &&
                (!hasStyle || deepEquals(cell === null || cell === void 0 ? void 0 : cell.style, cmd.style)) &&
                (!hasFormat || (cell === null || cell === void 0 ? void 0 : cell.format) === cmd.format)) {
                return 89 /* CommandResult.NoChanges */;
            }
            return 0 /* CommandResult.Success */;
        }
    }
    CellPlugin.getters = [
        "zoneToXC",
        "getCells",
        "getFormulaCellContent",
        "getCellStyle",
        "buildFormulaContent",
        "getCellById",
    ];

    class ChartPlugin extends CorePlugin {
        constructor() {
            super(...arguments);
            this.charts = {};
            this.createChart = chartFactory(this.getters);
            this.validateChartDefinition = (cmd) => validateChartDefinition(this, cmd.definition);
        }
        adaptRanges(applyChange) {
            for (const [chartId, chart] of Object.entries(this.charts)) {
                this.history.update("charts", chartId, chart === null || chart === void 0 ? void 0 : 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 0 /* CommandResult.Success */;
            }
        }
        handle(cmd) {
            var _a;
            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 = (_a = this.charts[fig.id]) === null || _a === void 0 ? void 0 : _a.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) {
            var _a;
            return (_a = this.charts[figureId]) === null || _a === void 0 ? void 0 : _a.getContextCreation();
        }
        getChart(figureId) {
            return this.charts[figureId];
        }
        getChartType(figureId) {
            var _a;
            const type = (_a = this.charts[figureId]) === null || _a === void 0 ? void 0 : _a.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 === null || chart === void 0 ? void 0 : chart.sheetId) === sheetId)
                .map(([id]) => id);
        }
        getChartDefinition(figureId) {
            var _a;
            const definition = (_a = this.charts[figureId]) === null || _a === void 0 ? void 0 : _a.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) {
            var _a;
            if (data.sheets) {
                for (let sheet of data.sheets) {
                    // TODO This code is false, if two plugins want ot 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 = (_a = this.charts[figure.id]) === null || _a === void 0 ? void 0 : _a.getDefinition();
                            if (data) {
                                figure.data = data;
                                figures.push(figure);
                            }
                        }
                        else {
                            figures.push(figure);
                        }
                    }
                    sheet.figures = figures;
                }
            }
        }
        exportForExcel(data) {
            var _a;
            for (let sheet of data.sheets) {
                const sheetFigures = this.getters.getFigures(sheet.id);
                const figures = [];
                for (let figure of sheetFigures) {
                    if (figure && figure.tag === "chart") {
                        const figureData = (_a = this.charts[figure.id]) === null || _a === void 0 ? void 0 : _a.getDefinitionForExcel();
                        if (figureData) {
                            figures.push({
                                ...figure,
                                data: figureData,
                            });
                        }
                    }
                }
                sheet.charts = 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)
                ? 85 /* CommandResult.DuplicatedChartId */
                : 0 /* CommandResult.Success */;
        }
        checkChartExists(cmd) {
            return this.isChartDefined(cmd.id) ? 0 /* CommandResult.Success */ : 86 /* CommandResult.ChartDoesNotExist */;
        }
    }
    ChartPlugin.getters = [
        "isChartDefined",
        "getChartDefinition",
        "getChartType",
        "getChartIds",
        "getChart",
        "getContextCreationChart",
    ];

    // -----------------------------------------------------------------------------
    // Constants
    // -----------------------------------------------------------------------------
    function stringToNumber(value) {
        return value === "" ? NaN : Number(value);
    }
    class ConditionalFormatPlugin extends CorePlugin {
        constructor() {
            super(...arguments);
            this.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 "MOVE_CONDITIONAL_FORMAT":
                    return this.checkValidReordering(cmd.cfId, cmd.direction, cmd.sheetId);
            }
            return 0 /* 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 "MOVE_CONDITIONAL_FORMAT":
                    this.reorderConditionalFormatting(cmd.cfId, cmd.direction, 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) {
            var _a;
            return ((_a = this.cfRules[sheetId]) === null || _a === void 0 ? void 0 : _a.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);
            }));
        }
        // ---------------------------------------------------------------------------
        // Private
        // ---------------------------------------------------------------------------
        mapToConditionalFormat(sheetId, cf, options = { useFixedReference: false }) {
            return {
                ...cf,
                ranges: cf.ranges.map((range) => {
                    return this.getters.getRangeString(range, sheetId, options.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);
        }
        checkValidReordering(cfId, direction, sheetId) {
            if (!this.cfRules[sheetId])
                return 27 /* CommandResult.InvalidSheetId */;
            const ruleIndex = this.cfRules[sheetId].findIndex((cf) => cf.id === cfId);
            if (ruleIndex === -1)
                return 72 /* CommandResult.InvalidConditionalFormatId */;
            const cfIndex2 = direction === "up" ? ruleIndex - 1 : ruleIndex + 1;
            if (cfIndex2 < 0 || cfIndex2 >= this.cfRules[sheetId].length) {
                return 72 /* CommandResult.InvalidConditionalFormatId */;
            }
            return 0 /* CommandResult.Success */;
        }
        checkEmptyRange(cmd) {
            return cmd.ranges.length ? 0 /* CommandResult.Success */ : 24 /* 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 0 /* 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(52 /* CommandResult.FirstArgMissing */);
                    }
                    if (expectedNumber >= 2 && isEmpty(rule.values[1])) {
                        errors.push(53 /* CommandResult.SecondArgMissing */);
                    }
                    return errors.length ? errors : 0 /* CommandResult.Success */;
                }
                return 0 /* CommandResult.Success */;
            };
        }
        checkNaN(threshold, thresholdName) {
            if (["number", "percentage", "percentile"].includes(threshold.type) &&
                (threshold.value === "" || isNaN(threshold.value))) {
                switch (thresholdName) {
                    case "min":
                        return 54 /* CommandResult.MinNaN */;
                    case "max":
                        return 56 /* CommandResult.MaxNaN */;
                    case "mid":
                        return 55 /* CommandResult.MidNaN */;
                    case "upperInflectionPoint":
                        return 57 /* CommandResult.ValueUpperInflectionNaN */;
                    case "lowerInflectionPoint":
                        return 58 /* CommandResult.ValueLowerInflectionNaN */;
                }
            }
            return 0 /* CommandResult.Success */;
        }
        checkFormulaCompilation(threshold, thresholdName) {
            if (threshold.type !== "formula")
                return 0 /* CommandResult.Success */;
            try {
                compile(threshold.value || "");
            }
            catch (error) {
                switch (thresholdName) {
                    case "min":
                        return 59 /* CommandResult.MinInvalidFormula */;
                    case "max":
                        return 61 /* CommandResult.MaxInvalidFormula */;
                    case "mid":
                        return 60 /* CommandResult.MidInvalidFormula */;
                    case "upperInflectionPoint":
                        return 62 /* CommandResult.ValueUpperInvalidFormula */;
                    case "lowerInflectionPoint":
                        return 63 /* CommandResult.ValueLowerInvalidFormula */;
                }
            }
            return 0 /* CommandResult.Success */;
        }
        checkThresholds(check) {
            return this.batchValidations((rule) => check(rule.minimum, "min"), (rule) => check(rule.maximum, "max"), (rule) => (rule.midpoint ? check(rule.midpoint, "mid") : 0 /* 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 49 /* CommandResult.LowerBiggerThanUpper */;
            }
            return 0 /* 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 48 /* CommandResult.MinBiggerThanMax */;
            }
            return 0 /* CommandResult.Success */;
        }
        checkMidBiggerThanMax(rule) {
            var _a;
            const midValue = (_a = rule.midpoint) === null || _a === void 0 ? void 0 : _a.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 50 /* CommandResult.MidBiggerThanMax */;
            }
            return 0 /* CommandResult.Success */;
        }
        checkMinBiggerThanMid(rule) {
            var _a;
            const minValue = rule.minimum.value;
            const midValue = (_a = rule.midpoint) === null || _a === void 0 ? void 0 : _a.value;
            if (rule.midpoint &&
                ["number", "percentage", "percentile"].includes(rule.midpoint.type) &&
                rule.minimum.type === rule.midpoint.type &&
                stringToNumber(minValue) >= stringToNumber(midValue)) {
                return 51 /* CommandResult.MinBiggerThanMid */;
            }
            return 0 /* 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);
            }
        }
        reorderConditionalFormatting(cfId, direction, sheetId) {
            const cfIndex1 = this.cfRules[sheetId].findIndex((s) => s.id === cfId);
            const cfIndex2 = direction === "up" ? cfIndex1 - 1 : cfIndex1 + 1;
            if (cfIndex2 < 0 || cfIndex2 >= this.cfRules[sheetId].length)
                return;
            if (cfIndex1 !== -1 && cfIndex2 !== -1) {
                const currentCF = [...this.cfRules[sheetId]];
                const tmp = currentCF[cfIndex1];
                currentCF[cfIndex1] = currentCF[cfIndex2];
                currentCF[cfIndex2] = tmp;
                this.history.update("cfRules", sheetId, currentCF);
            }
        }
    }
    ConditionalFormatPlugin.getters = ["getConditionalFormats", "getRulesSelection", "getRulesByCell"];

    class FigurePlugin extends CorePlugin {
        constructor() {
            super(...arguments);
            this.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 0 /* CommandResult.Success */;
            }
        }
        beforeHandle(cmd) {
            switch (cmd.type) {
                case "DELETE_SHEET":
                    this.getters.getFigures(cmd.sheetId).forEach((figure) => {
                        this.dispatch("DELETE_FIGURE", { id: figure.id, sheetId: cmd.sheetId });
                    });
                    break;
            }
        }
        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;
            }
        }
        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) {
            var _a;
            if (((_a = this.figures[sheetId]) === null || _a === void 0 ? void 0 : _a[figureId]) === undefined) {
                return 71 /* CommandResult.FigureDoesNotExist */;
            }
            return 0 /* CommandResult.Success */;
        }
        checkFigureDuplicate(figureId) {
            if (Object.values(this.figures).find((sheet) => sheet === null || sheet === void 0 ? void 0 : sheet[figureId])) {
                return 83 /* CommandResult.DuplicatedFigureId */;
            }
            return 0 /* CommandResult.Success */;
        }
        // ---------------------------------------------------------------------------
        // Getters
        // ---------------------------------------------------------------------------
        getFigures(sheetId) {
            return Object.values(this.figures[sheetId] || {}).filter(isDefined$1);
        }
        getFigure(sheetId, figureId) {
            var _a;
            return (_a = this.figures[sheetId]) === null || _a === void 0 ? void 0 : _a[figureId];
        }
        getFigureSheetId(figureId) {
            return Object.keys(this.figures).find((sheetId) => { var _a; return ((_a = this.figures[sheetId]) === null || _a === void 0 ? void 0 : _a[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);
        }
    }
    FigurePlugin.getters = ["getFigures", "getFigure", "getFigureSheetId"];

    class FilterTable {
        constructor(zone) {
            this.filters = [];
            this.zone = zone;
            const uuid = new UuidGenerator();
            this.id = uuid.smallUuid();
            for (const i of range(zone.left, zone.right + 1)) {
                const filterZone = { ...this.zone, left: i, right: i };
                this.filters.push(new Filter(uuid.smallUuid(), 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) {
            var _a;
            return (_a = this.filters.find((filter) => filter.col === col)) === null || _a === void 0 ? void 0 : _a.id;
        }
        clone() {
            return new FilterTable(this.zone);
        }
    }
    class Filter {
        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 {
        constructor() {
            super(...arguments);
            this.tables = {};
        }
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "CREATE_FILTER_TABLE":
                    if (!areZonesContinuous(...cmd.target)) {
                        return 82 /* CommandResult.NonContinuousTargets */;
                    }
                    const zone = union(...cmd.target);
                    const checkFilterOverlap = () => {
                        if (this.getFilterTables(cmd.sheetId).some((filter) => overlap(filter.zone, zone))) {
                            return 79 /* CommandResult.FilterOverlap */;
                        }
                        return 0 /* CommandResult.Success */;
                    };
                    const checkMergeInFilter = () => {
                        const mergesInTarget = this.getters.getMergesInZone(cmd.sheetId, zone);
                        for (let merge of mergesInTarget) {
                            if (overlap(zone, merge)) {
                                return 81 /* CommandResult.MergeInFilter */;
                            }
                        }
                        return 0 /* 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 81 /* CommandResult.MergeInFilter */;
                            }
                        }
                    }
                    break;
            }
            return 0 /* 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(sheetId, col, row) {
            var _a;
            return (_a = this.getFilterTable(sheetId, col, row)) === null || _a === void 0 ? void 0 : _a.filters.find((filter) => filter.col === col);
        }
        getFilterId(sheetId, col, row) {
            var _a;
            return (_a = this.getFilter(sheetId, col, row)) === null || _a === void 0 ? void 0 : _a.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;
        }
        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).width) {
                    for (let col = zone.left; col <= zone.right; col++) {
                        if (!filters.find((filter) => filter.col === col)) {
                            filters.push(new Filter(this.uuidGenerator.smallUuid(), { ...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) {
            var _a;
            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)) {
                // Since this plugin is loaded before CellPlugin, the getters still give us the old cell content
                const cellContent = (_a = this.getters.getCell(sheetId, col, row)) === null || _a === void 0 ? void 0 : _a.content;
                if (cellContent) {
                    return false;
                }
                if (this.getters.getFilter(sheetId, col, row)) {
                    return false;
                }
                if (this.getters.isInMerge(sheetId, col, row)) {
                    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) {
            for (const sheet of data.sheets) {
                for (const filterTable of this.getFilterTables(sheet.id)) {
                    if (zoneToDimension(filterTable.zone).height === 1) {
                        continue;
                    }
                    sheet.filterTables.push({
                        range: zoneToXc(filterTable.zone),
                        filters: [],
                    });
                }
            }
        }
    }
    FiltersPlugin.getters = [
        "doesZonesContainFilter",
        "getFilter",
        "getFilters",
        "getFilterTable",
        "getFilterTables",
        "getFilterTablesInZone",
        "getFilterId",
    ];

    class HeaderSizePlugin extends CorePlugin {
        constructor() {
            super(...arguments);
            this.sizes = {};
        }
        handle(cmd) {
            var _a, _b;
            switch (cmd.type) {
                case "CREATE_SHEET": {
                    const computedSizes = this.computeSheetSizes(cmd.sheetId);
                    const sizes = {
                        COL: computedSizes.COL.map((size) => ({
                            manualSize: undefined,
                            computedSize: lazy(size),
                        })),
                        ROW: computedSizes.ROW.map((size) => ({
                            manualSize: undefined,
                            computedSize: lazy(size),
                        })),
                    };
                    this.history.update("sizes", cmd.sheetId, sizes);
                    break;
                }
                case "DUPLICATE_SHEET":
                    // make sure the values are computed in case the original sheet is deleted
                    for (const row of this.sizes[cmd.sheetId].ROW) {
                        row.computedSize();
                    }
                    for (const col of this.sizes[cmd.sheetId].COL) {
                        col.computedSize();
                    }
                    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": {
                    let sizes = [...this.sizes[cmd.sheetId][cmd.dimension]];
                    for (let headerIndex of [...cmd.elements].sort((a, b) => b - a)) {
                        sizes.splice(headerIndex, 1);
                    }
                    const min = largeMin(cmd.elements);
                    sizes = sizes.map((size, row) => {
                        if (cmd.dimension === "ROW" && row >= min) {
                            // invalidate sizes
                            return {
                                manualSize: size.manualSize,
                                computedSize: lazy(() => this.getRowTallestCellSize(cmd.sheetId, row)),
                            };
                        }
                        return size;
                    });
                    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];
                    const sizesToInsert = range(0, cmd.quantity).map(() => ({
                        manualSize: baseSize.manualSize,
                        computedSize: lazy(baseSize.computedSize()),
                    }));
                    sizes.splice(addIndex, 0, ...sizesToInsert);
                    sizes = sizes.map((size, row) => {
                        if (cmd.dimension === "ROW" && row > cmd.base + cmd.quantity) {
                            // invalidate sizes
                            return {
                                manualSize: size.manualSize,
                                computedSize: lazy(() => this.getRowTallestCellSize(cmd.sheetId, row)),
                            };
                        }
                        return size;
                    });
                    this.history.update("sizes", cmd.sheetId, cmd.dimension, sizes);
                    break;
                }
                case "RESIZE_COLUMNS_ROWS":
                    for (let el of cmd.elements) {
                        if (cmd.dimension === "ROW") {
                            const height = this.getRowTallestCellSize(cmd.sheetId, el);
                            const size = height;
                            this.history.update("sizes", cmd.sheetId, cmd.dimension, el, {
                                manualSize: cmd.size || undefined,
                                computedSize: lazy(size),
                            });
                        }
                        else {
                            this.history.update("sizes", cmd.sheetId, cmd.dimension, el, {
                                manualSize: cmd.size || undefined,
                                computedSize: lazy(cmd.size || DEFAULT_CELL_WIDTH),
                            });
                        }
                    }
                    break;
                case "UPDATE_CELL":
                    const row = (_b = (_a = this.sizes[cmd.sheetId]) === null || _a === void 0 ? void 0 : _a["ROW"]) === null || _b === void 0 ? void 0 : _b[cmd.row];
                    if (row && !row.manualSize) {
                        const { sheetId, row } = cmd;
                        this.history.update("sizes", sheetId, "ROW", row, "computedSize", lazy(() => this.getRowTallestCellSize(sheetId, row)));
                    }
                    break;
                case "ADD_MERGE":
                case "REMOVE_MERGE":
                    for (let target of cmd.target) {
                        for (let row of range(target.top, target.bottom + 1)) {
                            const rowHeight = this.getRowTallestCellSize(cmd.sheetId, row);
                            if (rowHeight !== this.getRowSize(cmd.sheetId, row)) {
                                this.history.update("sizes", cmd.sheetId, "ROW", row, "computedSize", lazy(rowHeight));
                            }
                        }
                    }
                    break;
            }
            return;
        }
        getColSize(sheetId, index) {
            return this.getHeaderSize(sheetId, "COL", index);
        }
        getRowSize(sheetId, index) {
            return this.getHeaderSize(sheetId, "ROW", index);
        }
        getHeaderSize(sheetId, dimension, index) {
            var _a, _b, _c, _d;
            return Math.round(((_b = (_a = this.sizes[sheetId]) === null || _a === void 0 ? void 0 : _a[dimension][index]) === null || _b === void 0 ? void 0 : _b.manualSize) ||
                ((_d = (_c = this.sizes[sheetId]) === null || _c === void 0 ? void 0 : _c[dimension][index]) === null || _d === void 0 ? void 0 : _d.computedSize()) ||
                this.getDefaultHeaderSize(dimension));
        }
        computeSheetSizes(sheetId) {
            var _a, _b;
            const sizes = { COL: [], ROW: [] };
            for (let col of range(0, this.getters.getNumberCols(sheetId))) {
                sizes.COL.push(this.getHeaderSize(sheetId, "COL", col));
            }
            for (let row of range(0, this.getters.getNumberRows(sheetId))) {
                let rowSize = (_b = (_a = this.sizes[sheetId]) === null || _a === void 0 ? void 0 : _a["ROW"]) === null || _b === void 0 ? void 0 : _b[row].manualSize;
                if (!rowSize) {
                    const height = this.getRowTallestCellSize(sheetId, row);
                    rowSize = height;
                }
                sizes.ROW.push(rowSize);
            }
            return sizes;
        }
        getDefaultHeaderSize(dimension) {
            return dimension === "COL" ? DEFAULT_CELL_WIDTH : DEFAULT_CELL_HEIGHT;
        }
        /**
         * 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 font size.
         */
        getCellHeight(sheetId, col, row) {
            const merge = this.getters.getMerge(sheetId, col, row);
            if (merge && merge.bottom !== merge.top) {
                return DEFAULT_CELL_HEIGHT;
            }
            const cell = this.getters.getCell(sheetId, col, row);
            // TO DO: take multiline cells into account to compute the cell height
            return getDefaultCellHeight(cell === null || cell === void 0 ? void 0 : cell.style);
        }
        /**
         * Get the tallest cell of a row and its size.
         *
         * The tallest cell of the row correspond to the cell with the biggest font size,
         * and that is not part of a multi-line merge.
         */
        getRowTallestCellSize(sheetId, row) {
            const cellIds = this.getters.getRowCells(sheetId, row);
            let maxHeight = 0;
            for (let i = 0; i < cellIds.length; i++) {
                const cell = this.getters.getCellById(cellIds[i]);
                if (!cell)
                    continue;
                const { col, row } = this.getters.getCellPosition(cell.id);
                const cellHeight = this.getCellHeight(sheetId, col, row);
                if (cellHeight > maxHeight && cellHeight > DEFAULT_CELL_HEIGHT) {
                    maxHeight = cellHeight;
                }
            }
            if (maxHeight <= DEFAULT_CELL_HEIGHT) {
                return DEFAULT_CELL_HEIGHT;
            }
            return maxHeight;
        }
        import(data) {
            for (let sheet of data.sheets) {
                const manualSizes = { COL: [], ROW: [] };
                for (let [rowIndex, row] of Object.entries(sheet.rows)) {
                    if (row.size) {
                        manualSizes["ROW"][rowIndex] = row.size;
                    }
                }
                for (let [colIndex, col] of Object.entries(sheet.cols)) {
                    if (col.size) {
                        manualSizes["COL"][colIndex] = col.size;
                    }
                }
                const computedSizes = this.computeSheetSizes(sheet.id);
                this.sizes[sheet.id] = {
                    COL: computedSizes.COL.map((size, i) => ({
                        manualSize: manualSizes.COL[i],
                        computedSize: lazy(size),
                    })),
                    ROW: computedSizes.ROW.map((size, i) => ({
                        manualSize: manualSizes.ROW[i],
                        computedSize: lazy(size),
                    })),
                };
            }
            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) {
            var _a, _b;
            for (let sheet of data.sheets) {
                // Export row sizes
                if (sheet.rows === undefined) {
                    sheet.rows = {};
                }
                for (let row of range(0, this.getters.getNumberRows(sheet.id))) {
                    if (exportDefaults || ((_a = this.sizes[sheet.id]["ROW"][row]) === null || _a === void 0 ? void 0 : _a.manualSize)) {
                        sheet.rows[row] = { ...sheet.rows[row], size: this.getRowSize(sheet.id, row) };
                    }
                }
                // Export col sizes
                if (sheet.cols === undefined) {
                    sheet.cols = {};
                }
                for (let col of range(0, this.getters.getNumberCols(sheet.id))) {
                    if (exportDefaults || ((_b = this.sizes[sheet.id]["COL"][col]) === null || _b === void 0 ? void 0 : _b.manualSize)) {
                        sheet.cols[col] = { ...sheet.cols[col], size: this.getColSize(sheet.id, col) };
                    }
                }
            }
        }
    }
    HeaderSizePlugin.getters = ["getRowSize", "getColSize"];

    class HeaderVisibilityPlugin extends CorePlugin {
        constructor() {
            super(...arguments);
            this.hiddenHeaders = {};
        }
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "HIDE_COLUMNS_ROWS": {
                    if (!this.getters.tryGetSheet(cmd.sheetId)) {
                        return 27 /* 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 67 /* CommandResult.TooManyHiddenElements */;
                    }
                    else if (largeMin(cmd.elements) < 0 || largeMax(cmd.elements) > elements) {
                        return 87 /* CommandResult.InvalidHeaderIndex */;
                    }
                    else {
                        return 0 /* CommandResult.Success */;
                    }
                }
                case "REMOVE_COLUMNS_ROWS":
                    if (!this.getters.tryGetSheet(cmd.sheetId)) {
                        return 27 /* CommandResult.InvalidSheetId */;
                    }
                    if (this.checkElementsIncludeAllVisibleHeaders(cmd.sheetId, cmd.dimension, cmd.elements)) {
                        return 8 /* CommandResult.NotEnoughElements */;
                    }
                    return 0 /* CommandResult.Success */;
            }
            return 0 /* 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);
        }
        isRowHiddenByUser(sheetId, index) {
            return this.hiddenHeaders[sheetId].ROW[index];
        }
        isColHiddenByUser(sheetId, index) {
            return this.hiddenHeaders[sheetId].COL[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) {
            return range(0, this.hiddenHeaders[sheetId][dimension].length).filter((i) => !this.hiddenHeaders[sheetId][dimension][i]);
        }
        import(data) {
            var _a, _b;
            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((_a = sheet.rows[row]) === null || _a === void 0 ? void 0 : _a.isHidden);
                }
                for (let col = 0; col < sheet.colNumber; col++) {
                    this.hiddenHeaders[sheet.id].COL[col] = Boolean((_b = sheet.cols[col]) === null || _b === void 0 ? void 0 : _b.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];
                    }
                }
            }
        }
    }
    HeaderVisibilityPlugin.getters = [
        "checkElementsIncludeAllVisibleHeaders",
        "getHiddenColsGroups",
        "getHiddenRowsGroups",
        "isRowHiddenByUser",
        "isColHiddenByUser",
    ];

    class MergePlugin extends CorePlugin {
        constructor() {
            super(...arguments);
            this.nextId = 1;
            this.merges = {};
            this.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 0 /* 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) {
            var _a;
            const sheetMap = this.mergeCellMap[sheetId];
            const mergeId = sheetMap ? col in sheetMap && ((_a = sheetMap[col]) === null || _a === void 0 ? void 0 : _a[row]) : undefined;
            return mergeId ? this.getMergeById(sheetId, mergeId) : undefined;
        }
        getMergesInZone(sheetId, zone) {
            var _a;
            const sheetMap = this.mergeCellMap[sheetId];
            if (!sheetMap)
                return [];
            const mergeIds = new Set();
            for (const { col, row } of positions(zone)) {
                const mergeId = (_a = sheetMap[col]) === null || _a === void 0 ? void 0 : _a[row];
                if (mergeId) {
                    mergeIds.add(mergeId);
                }
            }
            return Array.from(mergeIds)
                .map((mergeId) => this.getMergeById(sheetId, mergeId))
                .filter(isDefined$1);
        }
        /**
         * Return true if the zone intersects an existing merge:
         * if they have at least a common cell
         */
        doesIntersectMerge(sheetId, zone) {
            const { left, right, top, bottom } = zone;
            for (let row = top; row <= bottom; row++) {
                for (let col = left; col <= right; col++) {
                    if (this.getMerge(sheetId, col, row)) {
                        return true;
                    }
                }
            }
            return false;
        }
        /**
         * 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, colA, rowA);
            const mergeB = this.getMerge(sheetId, colB, rowB);
            if (!mergeA || !mergeB) {
                return false;
            }
            return isEqual(mergeA, mergeB);
        }
        isInMerge(sheetId, col, row) {
            var _a;
            const sheetMap = this.mergeCellMap[sheetId];
            return sheetMap ? col in sheetMap && Boolean((_a = sheetMap[col]) === null || _a === void 0 ? void 0 : _a[row]) : false;
        }
        getMainCellPosition(sheetId, col, row) {
            if (!this.isInMerge(sheetId, col, row)) {
                return { col, row };
            }
            const mergeTopLeftPos = this.getMerge(sheetId, col, row).topLeft;
            return { col: mergeTopLeftPos.col, row: mergeTopLeftPos.row };
        }
        getBottomLeftCell(sheetId, col, row) {
            if (!this.isInMerge(sheetId, col, row)) {
                return { col, row };
            }
            const { bottom, left } = this.getMerge(sheetId, col, row);
            return { 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, zone.left, zone.top);
            if (merge) {
                return isEqual(zone, merge);
            }
            const { width, height } = zoneToDimension(zone);
            return width === 1 && height === 1;
        }
        // ---------------------------------------------------------------------------
        // 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.isEmpty()) {
                            return true;
                        }
                    }
                }
            }
            return false;
        }
        getMergeById(sheetId, mergeId) {
            var _a;
            const range = (_a = this.merges[sheetId]) === null || _a === void 0 ? void 0 : _a[mergeId];
            return range !== undefined ? rangeToMerge(mergeId, range) : undefined;
        }
        checkDestructiveMerge({ sheetId, target }) {
            const sheet = this.getters.tryGetSheet(sheetId);
            if (!sheet)
                return 0 /* CommandResult.Success */;
            const isDestructive = target.some((zone) => this.isMergeDestructive(sheetId, zone));
            return isDestructive ? 3 /* CommandResult.MergeIsDestructive */ : 0 /* CommandResult.Success */;
        }
        checkOverlap({ target }) {
            for (const zone of target) {
                for (const zone2 of target) {
                    if (zone !== zone2 && overlap(zone, zone2)) {
                        return 66 /* CommandResult.MergeOverlap */;
                    }
                }
            }
            return 0 /* CommandResult.Success */;
        }
        checkFrozenPanes({ sheetId, target }) {
            const sheet = this.getters.tryGetSheet(sheetId);
            if (!sheet)
                return 0 /* 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 76 /* CommandResult.FrozenPaneOverlap */;
                }
            }
            return 0 /* CommandResult.Success */;
        }
        /**
         * The content of a merged cell should always be empty.
         * Except for the top-left cell.
         */
        checkMergedContentUpdate(cmd) {
            const { col, row, sheetId, content } = cmd;
            if (content === undefined) {
                return 0 /* CommandResult.Success */;
            }
            const { col: mainCol, row: mainRow } = this.getMainCellPosition(sheetId, col, row);
            if (mainCol === col && mainRow === row) {
                return 0 /* CommandResult.Success */;
            }
            return 4 /* CommandResult.CellIsMerged */;
        }
        checkMergeExists(cmd) {
            const { sheetId, target } = cmd;
            for (const zone of target) {
                const { left, top } = zone;
                const merge = this.getMerge(sheetId, left, top);
                if (merge === undefined || !isEqual(zone, merge)) {
                    return 5 /* CommandResult.InvalidTarget */;
                }
            }
            return 0 /* 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, left, 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 r = top; r <= bottom; r++) {
                    for (let c = left; c <= right; c++) {
                        const merge = this.getMerge(sheetId, c, r);
                        if (!merge || merge.id !== id) {
                            this.history.update("mergeCellMap", sheetId, c, r, undefined);
                            this.dispatch("CLEAR_CELL", {
                                sheetId,
                                col: c,
                                row: r,
                            });
                        }
                    }
                }
                this.history.update("merges", sheetId, mergeId, undefined);
            }
        }
        removeMerge(sheetId, zone) {
            const { left, top, bottom, right } = zone;
            const merge = this.getMerge(sheetId, left, 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 { width, height } = zoneToDimension(result.range.zone);
                            if (width === 1 && height === 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);
        }
    }
    MergePlugin.getters = [
        "isInMerge",
        "isInSameMerge",
        "isMergeHidden",
        "getMainCellPosition",
        "getBottomLeftCell",
        "expandZone",
        "doesIntersectMerge",
        "doesColumnsHaveCommonMerges",
        "doesRowsHaveCommonMerges",
        "getMerges",
        "getMerge",
        "getMergesInZone",
        "isSingleCellOrMerge",
    ];
    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 SheetPlugin extends CorePlugin {
        constructor() {
            super(...arguments);
            this.sheetIdsMapName = {};
            this.orderedSheetIds = [];
            this.sheets = {};
            this.cellPosition = {};
        }
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        allowDispatch(cmd) {
            const genericChecks = this.chainValidations(this.checkSheetExists, this.checkZones)(cmd);
            if (genericChecks !== 0 /* CommandResult.Success */) {
                return genericChecks;
            }
            switch (cmd.type) {
                case "HIDE_SHEET": {
                    if (this.getVisibleSheetIds().length === 1) {
                        return 9 /* CommandResult.NotEnoughSheets */;
                    }
                    return 0 /* CommandResult.Success */;
                }
                case "CREATE_SHEET": {
                    return this.checkValidations(cmd, this.checkSheetName, this.checkSheetPosition);
                }
                case "DUPLICATE_SHEET": {
                    if (this.sheets[cmd.sheetIdTo])
                        return 12 /* CommandResult.DuplicatedSheetId */;
                    if (this.orderedSheetIds.map(this.getSheetName.bind(this)).includes(cmd.sheetNameTo))
                        return 11 /* CommandResult.DuplicatedSheetName */;
                    return 0 /* CommandResult.Success */;
                }
                case "MOVE_SHEET":
                    const currentIndex = this.orderedSheetIds.indexOf(cmd.sheetId);
                    if (cmd.direction === "left") {
                        const leftSheets = this.orderedSheetIds
                            .slice(0, currentIndex)
                            .map((id) => !this.isSheetVisible(id));
                        return leftSheets.every((isHidden) => isHidden)
                            ? 14 /* CommandResult.WrongSheetMove */
                            : 0 /* CommandResult.Success */;
                    }
                    else {
                        const rightSheets = this.orderedSheetIds
                            .slice(currentIndex + 1)
                            .map((id) => !this.isSheetVisible(id));
                        return rightSheets.every((isHidden) => isHidden)
                            ? 14 /* CommandResult.WrongSheetMove */
                            : 0 /* CommandResult.Success */;
                    }
                case "RENAME_SHEET":
                    return this.isRenameAllowed(cmd);
                case "DELETE_SHEET":
                    return this.getVisibleSheetIds().length > 1
                        ? 0 /* CommandResult.Success */
                        : 9 /* CommandResult.NotEnoughSheets */;
                case "ADD_COLUMNS_ROWS":
                    const elements = cmd.dimension === "COL"
                        ? this.getNumberCols(cmd.sheetId)
                        : this.getNumberRows(cmd.sheetId);
                    if (cmd.base < 0 || cmd.base >= elements) {
                        return 87 /* CommandResult.InvalidHeaderIndex */;
                    }
                    else if (cmd.quantity <= 0) {
                        return 88 /* CommandResult.InvalidQuantity */;
                    }
                    return 0 /* CommandResult.Success */;
                case "REMOVE_COLUMNS_ROWS": {
                    const elements = cmd.dimension === "COL"
                        ? this.getNumberCols(cmd.sheetId)
                        : this.getNumberRows(cmd.sheetId);
                    if (largeMin(cmd.elements) < 0 || largeMax(cmd.elements) > elements) {
                        return 87 /* CommandResult.InvalidHeaderIndex */;
                    }
                    else if (this.checkElementsIncludeAllNonFrozenHeaders(cmd.sheetId, cmd.dimension, cmd.elements)) {
                        return 8 /* CommandResult.NotEnoughElements */;
                    }
                    else {
                        return 0 /* 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 0 /* 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.direction);
                    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, cmd.sheetNameTo);
                    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) {
            var _a, _b;
            // 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: ((_a = sheetData.panes) === null || _a === void 0 ? void 0 : _a.xSplit) || 0,
                        ySplit: ((_b = sheetData.panes) === null || _b === void 0 ? void 0 : _b.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;
        }
        getCellsInZone(sheetId, zone) {
            return positions(zone).map(({ col, row }) => this.getCell(sheetId, col, row));
        }
        /**
         * Return the sheet name or undefined if the sheet doesn't exist.
         */
        tryGetSheetName(sheetId) {
            var _a;
            return (_a = this.tryGetSheet(sheetId)) === null || _a === void 0 ? void 0 : _a.name;
        }
        getSheetIdByName(name) {
            if (name) {
                const unquotedName = getUnquotedSheetName(name);
                for (const key in this.sheetIdsMapName) {
                    if (isSheetNameEqual(key, unquotedName)) {
                        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);
        }
        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) {
            var _a;
            const sheet = this.tryGetSheet(sheetId);
            const cellId = (_a = sheet === null || sheet === void 0 ? void 0 : sheet.rows[row]) === null || _a === void 0 ? void 0 : _a.cells[col];
            if (cellId === undefined) {
                return undefined;
            }
            return this.getters.getCellById(cellId);
        }
        /**
         * Returns all the cells of a col
         */
        getColCells(sheetId, col) {
            return this.getSheet(sheetId)
                .rows.map((row) => row.cells[col])
                .filter(isDefined$1)
                .map((cellId) => this.getters.getCellById(cellId))
                .filter(isDefined$1);
        }
        getColsZone(sheetId, start, end) {
            return {
                top: 0,
                bottom: this.getNumberRows(sheetId) - 1,
                left: start,
                right: end,
            };
        }
        getRowCells(sheetId, row) {
            var _a;
            return Object.values((_a = this.getSheet(sheetId).rows[row]) === null || _a === void 0 ? void 0 : _a.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") {
            const names = this.orderedSheetIds.map(this.getSheetName.bind(this));
            return getNextSheetName(names, baseName);
        }
        getSheetSize(sheetId) {
            return {
                height: this.getNumberRows(sheetId),
                width: this.getNumberCols(sheetId),
            };
        }
        getSheetZone(sheetId) {
            return {
                top: 0,
                left: 0,
                bottom: this.getNumberRows(sheetId) - 1,
                right: this.getNumberCols(sheetId) - 1,
            };
        }
        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 this.getCellsInZone(sheetId, zone)
                .flat()
                .every((cell) => !cell || cell.isEmpty());
        }
        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) {
            var _a;
            const cellId = (_a = this.sheets[sheetId]) === null || _a === void 0 ? void 0 : _a.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, direction) {
            const orderedSheetIds = this.orderedSheetIds.slice();
            const currentIndex = orderedSheetIds.findIndex((id) => id === sheetId);
            const sheet = orderedSheetIds.splice(currentIndex, 1);
            let index = direction === "left"
                ? this.findIndexOfPreviousVisibleSheet(currentIndex - 1, orderedSheetIds)
                : this.findIndexOfNextVisibleSheet(currentIndex + 1, orderedSheetIds);
            if (index === undefined) {
                index = orderedSheetIds.length;
            }
            orderedSheetIds.splice(index, 0, sheet[0]);
            this.history.update("orderedSheetIds", orderedSheetIds);
        }
        findIndexOfPreviousVisibleSheet(current, orderedSheetIds) {
            while (current >= 0 && !this.isSheetVisible(orderedSheetIds[current])) {
                current--;
            }
            if (current === -1) {
                throw new Error("There is no previous visible sheet");
            }
            return current;
        }
        findIndexOfNextVisibleSheet(current, orderedSheetIds) {
            while (current < orderedSheetIds.length && !this.isSheetVisible(orderedSheetIds[current])) {
                current++;
            }
            if (current === orderedSheetIds.length - 1 &&
                !this.isSheetVisible(orderedSheetIds[current - 1])) {
                return undefined;
            }
            return current;
        }
        checkSheetName(cmd) {
            const { orderedSheetIds, sheets } = this;
            const name = cmd.name && cmd.name.trim().toLowerCase();
            if (orderedSheetIds.find((id) => { var _a; return isSheetNameEqual((_a = sheets[id]) === null || _a === void 0 ? void 0 : _a.name, name); })) {
                return 11 /* CommandResult.DuplicatedSheetName */;
            }
            if (FORBIDDEN_IN_EXCEL_REGEX.test(name)) {
                return 13 /* CommandResult.ForbiddenCharactersInSheetName */;
            }
            return 0 /* CommandResult.Success */;
        }
        checkSheetPosition(cmd) {
            const { orderedSheetIds } = this;
            if (cmd.position > orderedSheetIds.length || cmd.position < 0) {
                return 15 /* CommandResult.WrongSheetPosition */;
            }
            return 0 /* CommandResult.Success */;
        }
        checkRowFreezeQuantity(cmd) {
            return cmd.quantity >= 1 && cmd.quantity < this.getNumberRows(cmd.sheetId)
                ? 0 /* CommandResult.Success */
                : 75 /* CommandResult.InvalidFreezeQuantity */;
        }
        checkColFreezeQuantity(cmd) {
            return cmd.quantity >= 1 && cmd.quantity < this.getNumberCols(cmd.sheetId)
                ? 0 /* CommandResult.Success */
                : 75 /* 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 66 /* CommandResult.MergeOverlap */;
                }
            }
            return 0 /* 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 66 /* CommandResult.MergeOverlap */;
                }
            }
            return 0 /* CommandResult.Success */;
        }
        isRenameAllowed(cmd) {
            const name = cmd.name && cmd.name.trim().toLowerCase();
            if (!name) {
                return 10 /* 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);
            sheetIdsMapName[name] = sheet.id;
            delete sheetIdsMapName[oldName];
            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, toName) {
            const sheet = this.getSheet(fromId);
            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) {
            const names = this.orderedSheetIds.map(this.getSheetName.bind(this));
            return getDuplicateSheetName(sheetName, names);
        }
        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.setNewPosition(cellId, sheet.id, colIndex - 1, rowIndex);
                        }
                    }
                }
            }
        }
        /**
         * Move the cells after a column or rows insertion
         */
        moveCellsOnAddition(sheet, addedElement, quantity, dimension) {
            const updates = [];
            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) {
                                updates.push({
                                    sheetId: sheet.id,
                                    cellId: cellId,
                                    col: colIndex + (dimension === "columns" ? quantity : 0),
                                    row: rowIndex + (dimension === "rows" ? quantity : 0),
                                    type: "UPDATE_CELL_POSITION",
                                });
                            }
                        }
                    }
                }
            }
            for (let update of updates.reverse()) {
                this.updateCellPosition(update);
            }
        }
        /**
         * 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.setNewPosition(cellId, sheet.id, colIndex, 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 };
        }
        // ----------------------------------------------------
        //  HIDE / SHOW
        // ----------------------------------------------------
        /**
         * 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 27 /* CommandResult.InvalidSheetId */;
            }
            else if (cmd.type === "CREATE_SHEET" && this.sheets[cmd.sheetId] !== undefined) {
                return 12 /* CommandResult.DuplicatedSheetId */;
            }
            return 0 /* CommandResult.Success */;
        }
        /**
         * Check if zones in the command are well formed and
         * not outside the sheet.
         */
        checkZones(cmd) {
            const zones = [];
            if ("zone" in cmd) {
                zones.push(cmd.zone);
            }
            if ("target" in cmd && Array.isArray(cmd.target)) {
                zones.push(...cmd.target);
            }
            if ("ranges" in cmd && Array.isArray(cmd.ranges)) {
                if (cmd.ranges.some((rangeData) => !this.getters.tryGetSheet(rangeData._sheetId))) {
                    return 27 /* CommandResult.InvalidSheetId */;
                }
                zones.push(...cmd.ranges.map((rangeData) => this.getters.getRangeFromRangeData(rangeData).zone));
            }
            if (!zones.every(isZoneValid)) {
                return 25 /* CommandResult.InvalidRange */;
            }
            else if (zones.length && "sheetId" in cmd) {
                const sheetZone = this.getSheetZone(cmd.sheetId);
                return zones.every((zone) => isZoneInside(zone, sheetZone))
                    ? 0 /* CommandResult.Success */
                    : 18 /* CommandResult.TargetOutOfSheet */;
            }
            return 0 /* CommandResult.Success */;
        }
    }
    SheetPlugin.getters = [
        "getSheetName",
        "tryGetSheetName",
        "getSheet",
        "tryGetSheet",
        "getSheetIdByName",
        "getSheetIds",
        "getVisibleSheetIds",
        "isSheetVisible",
        "getEvaluationSheets",
        "doesHeaderExist",
        "getCell",
        "getCellsInZone",
        "getCellPosition",
        "getColCells",
        "getColsZone",
        "getRowCells",
        "getRowsZone",
        "getNumberCols",
        "getNumberRows",
        "getNumberHeaders",
        "getGridLinesVisibility",
        "getNextSheetName",
        "isEmpty",
        "getSheetSize",
        "getSheetZone",
        "getPaneDivisions",
        "checkElementsIncludeAllNonFrozenHeaders",
        "getDuplicateSheetName",
    ];

    /**
     * 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 {
        constructor(cells, getters, direction) {
            this.index = 0;
            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 {
        constructor() {
            super(...arguments);
            this.lastCellSelected = {};
        }
        // ---------------------------------------------------------------------------
        // 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 0 /* CommandResult.Success */;
                    }
                    return 46 /* CommandResult.InvalidAutofillSelection */;
                case "AUTOFILL_AUTO":
                    const zone = this.getters.getSelectedZone();
                    return zone.top === zone.bottom
                        ? 0 /* CommandResult.Success */
                        : 1 /* CommandResult.CancelledForUnknownReason */;
            }
            return 0 /* 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,
                    });
            }
        }
        // ---------------------------------------------------------------------------
        // 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 left = this.getters.getCell(sheetId, col - 1, row);
                while (left && !left.isEmpty()) {
                    row += 1;
                    left = this.getters.getCell(sheetId, col - 1, row);
                }
            }
            if (row === zone.bottom) {
                col = zone.right;
                if (col <= this.getters.getNumberCols(sheetId)) {
                    let right = this.getters.getCell(sheetId, col + 1, row);
                    while (right && !right.isEmpty()) {
                        row += 1;
                        right = this.getters.getCell(sheetId, 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 && this.direction && rule.generateRule(cell, cells, this.direction);
        }
        /**
         * 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 { sheetId, col, row } = cellData;
                const border = this.getters.getCellBorder(sheetId, col, row) || 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 activeSheet = this.getters.getActiveSheet();
            if (this.getters.isInMerge(activeSheet.id, col, row) &&
                !this.getters.isInMerge(activeSheet.id, originCol, originRow)) {
                const zone = this.getters.getMerge(activeSheet.id, col, row);
                if (zone) {
                    this.dispatch("REMOVE_MERGE", {
                        sheetId: activeSheet.id,
                        target: [zone],
                    });
                }
            }
            const originMerge = this.getters.getMerge(activeSheet.id, originCol, originRow);
            if ((originMerge === null || originMerge === void 0 ? void 0 : originMerge.topLeft.col) === originCol && (originMerge === null || originMerge === void 0 ? void 0 : originMerge.topLeft.row) === originRow) {
                this.dispatch("ADD_MERGE", {
                    sheetId: activeSheet.id,
                    target: [
                        {
                            top: row,
                            bottom: row + originMerge.bottom - originMerge.top,
                            left: col,
                            right: col + originMerge.right - originMerge.left,
                        },
                    ],
                });
            }
        }
        // ---------------------------------------------------------------------------
        // 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([]);
            }
        }
    }
    AutofillPlugin.layers = [6 /* LAYERS.Autofill */];
    AutofillPlugin.getters = ["getAutofillTooltip"];

    class AutomaticSumPlugin extends UIPlugin {
        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.getCell(sheet.id, col, row - 1);
            const leftCell = this.getters.getCell(sheet.id, 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.getCellsInZone(sheet.id, zone);
            const cellPositions = range(end, -1, -1);
            const invalidCells = cellPositions.filter((position) => { var _a; return cells[position] && !((_a = cells[position]) === null || _a === void 0 ? void 0 : _a.isAutoSummable); });
            const maxValidPosition = largeMax(invalidCells);
            const numberSequences = groupConsecutive(cellPositions.filter((position) => this.isNumber(cells[position])));
            const firstSequence = numberSequences[0] || [];
            if (largeMax(firstSequence) < maxValidPosition) {
                return Infinity;
            }
            return largeMin(firstSequence);
        }
        shouldFindData(sheetId, zone) {
            return this.getters.isEmpty(sheetId, zone) || this.getters.isSingleCellOrMerge(sheetId, zone);
        }
        isNumber(cell) {
            var _a;
            return ((cell === null || cell === void 0 ? void 0 : cell.evaluated.type) === CellValueType.number &&
                !((_a = cell.evaluated.format) === null || _a === void 0 ? void 0 : _a.match(DATETIME_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).width === 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")));
        }
    }
    AutomaticSumPlugin.getters = ["getAutomaticSums"];

    /**
     * Plugin managing the display of components next to cells.
     */
    class CellPopoverPlugin extends UIPlugin {
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "OPEN_CELL_POPOVER":
                    try {
                        cellPopoverRegistry.get(cmd.popoverType);
                    }
                    catch (error) {
                        return 73 /* CommandResult.InvalidCellPopover */;
                    }
                    return 0 /* CommandResult.Success */;
                default:
                    return 0 /* 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,
                        type: cmd.popoverType,
                    };
                    break;
                case "CLOSE_CELL_POPOVER":
                    this.persistentPopover = undefined;
                    break;
            }
        }
        getCellPopover({ col, row }) {
            var _a, _b;
            const sheetId = this.getters.getActiveSheetId();
            if (this.persistentPopover &&
                this.getters.isVisibleInViewport(sheetId, this.persistentPopover.col, this.persistentPopover.row)) {
                const mainPosition = this.getters.getMainCellPosition(sheetId, this.persistentPopover.col, this.persistentPopover.row);
                const popover = (_b = (_a = cellPopoverRegistry
                    .get(this.persistentPopover.type)).onOpen) === null || _b === void 0 ? void 0 : _b.call(_a, mainPosition, this.getters);
                return !(popover === null || popover === void 0 ? void 0 : popover.isOpen)
                    ? { isOpen: false }
                    : {
                        ...popover,
                        ...this.computePopoverProps(this.persistentPopover, popover.cellCorner),
                    };
            }
            if (col === undefined ||
                row === undefined ||
                !this.getters.isVisibleInViewport(sheetId, col, row)) {
                return { isOpen: false };
            }
            const mainPosition = this.getters.getMainCellPosition(sheetId, col, row);
            const popover = cellPopoverRegistry
                .getAll()
                .map((matcher) => { var _a; return (_a = matcher.onHover) === null || _a === void 0 ? void 0 : _a.call(matcher, mainPosition, this.getters); })
                .find((popover) => popover === null || popover === void 0 ? void 0 : popover.isOpen);
            return !(popover === null || popover === void 0 ? void 0 : popover.isOpen)
                ? { isOpen: false }
                : {
                    ...popover,
                    ...this.computePopoverProps(mainPosition, popover.cellCorner),
                };
        }
        getPersistentPopoverTypeAtPosition({ col, row }) {
            if (this.persistentPopover &&
                this.persistentPopover.col === col &&
                this.persistentPopover.row === row) {
                return this.persistentPopover.type;
            }
            return undefined;
        }
        computePopoverProps({ col, row }, corner) {
            const { width, height } = this.getters.getVisibleRect(positionToZone({ col, row }));
            return {
                coordinates: this.computePopoverPosition({ col, row }, corner),
                cellWidth: -width,
                cellHeight: -height,
            };
        }
        computePopoverPosition({ col, row }, corner) {
            const sheetId = this.getters.getActiveSheetId();
            const merge = this.getters.getMerge(sheetId, col, row);
            if (merge) {
                col = corner === "TopRight" ? merge.right : merge.left;
                row = corner === "TopRight" ? merge.top : merge.bottom;
            }
            // x, y are relative to the canvas
            const { x, y, width, height } = this.getters.getVisibleRect(positionToZone({ col, row }));
            switch (corner) {
                case "BottomLeft":
                    return { x, y: y + height };
                case "TopRight":
                    return { x: x + width, y: y };
            }
        }
    }
    CellPopoverPlugin.getters = ["getCellPopover", "getPersistentPopoverTypeAtPosition"];
    CellPopoverPlugin.modes = ["normal"];

    /** Abstract state of the clipboard when copying/cutting content that is pasted in cells of the sheet */
    class ClipboardCellsAbstractState {
        constructor(operation, getters, dispatch, selection) {
            this.getters = getters;
            this.dispatch = dispatch;
            this.selection = selection;
            this.operation = operation;
            this.sheetId = getters.getActiveSheetId();
        }
        isCutAllowed(target) {
            return 0 /* CommandResult.Success */;
        }
        isPasteAllowed(target, clipboardOption) {
            return 0 /* 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 {
        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) {
                    cellsInRow.push({
                        cell: getters.getCell(sheetId, col, row),
                        border: getters.getCellBorder(sheetId, col, row) || undefined,
                        position: { col, row, sheetId },
                    });
                }
                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, 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 19 /* CommandResult.WrongCutSelection */;
            }
            return 0 /* CommandResult.Success */;
        }
        isPasteAllowed(target, clipboardOption) {
            const sheetId = this.getters.getActiveSheetId();
            if (this.operation === "CUT" && (clipboardOption === null || clipboardOption === void 0 ? void 0 : clipboardOption.pasteOption) !== undefined) {
                // cannot paste only format or only value if the previous operation is a CUT
                return 21 /* 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 20 /* 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 2 /* 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 76 /* CommandResult.FrozenPaneOverlap */;
                }
            }
            return 0 /* 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 === null || options === void 0 ? void 0 : 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 === null || options === void 0 ? void 0 : 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 });
            }
        }
        /**
         * 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 === null || clipboardOptions === void 0 ? void 0 : clipboardOptions.pasteOption) !== "onlyValue" && (clipboardOptions === null || clipboardOptions === void 0 ? void 0 : clipboardOptions.shouldPasteCF);
            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];
                    const position = { col: col + c, row: row + r, sheetId: sheetId };
                    this.pasteMergeIfExist(origin.position, position, this.operation);
                    this.pasteCell(origin, position, this.operation, clipboardOptions);
                    if (shouldPasteCF) {
                        this.dispatch("PASTE_CONDITIONAL_FORMAT", {
                            origin: origin.position,
                            target: position,
                            operation: this.operation,
                        });
                    }
                }
            }
        }
        /**
         * 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.getCell(sheetId, col, row);
            if ((clipboardOption === null || clipboardOption === void 0 ? void 0 : clipboardOption.pasteOption) !== "onlyValue") {
                const targetBorders = this.getters.getCellBorder(sheetId, col, row);
                const originBorders = origin.border;
                const border = {
                    top: (targetBorders === null || targetBorders === void 0 ? void 0 : targetBorders.top) || (originBorders === null || originBorders === void 0 ? void 0 : originBorders.top),
                    bottom: (targetBorders === null || targetBorders === void 0 ? void 0 : targetBorders.bottom) || (originBorders === null || originBorders === void 0 ? void 0 : originBorders.bottom),
                    left: (targetBorders === null || targetBorders === void 0 ? void 0 : targetBorders.left) || (originBorders === null || originBorders === void 0 ? void 0 : originBorders.left),
                    right: (targetBorders === null || targetBorders === void 0 ? void 0 : targetBorders.right) || (originBorders === null || originBorders === void 0 ? void 0 : originBorders.right),
                };
                this.dispatch("SET_BORDER", { sheetId, col, row, border });
            }
            if (origin.cell) {
                if ((clipboardOption === null || clipboardOption === void 0 ? void 0 : clipboardOption.pasteOption) === "onlyFormat") {
                    this.dispatch("UPDATE_CELL", {
                        ...target,
                        style: origin.cell.style,
                        format: origin.cell.evaluated.format || origin.cell.format,
                    });
                    return;
                }
                if ((clipboardOption === null || clipboardOption === void 0 ? void 0 : clipboardOption.pasteOption) === "onlyValue") {
                    const content = formatValue(origin.cell.evaluated.value);
                    this.dispatch("UPDATE_CELL", { ...target, content });
                    return;
                }
                let content = origin.cell.content;
                if (origin.cell.isFormula() && operation === "COPY") {
                    const offsetX = col - origin.position.col;
                    const offsetY = row - origin.position.row;
                    content = this.getUpdatedContent(sheetId, origin.cell, offsetX, offsetY, operation);
                }
                this.dispatch("UPDATE_CELL", {
                    ...target,
                    content,
                    style: origin.cell.style || null,
                    format: origin.cell.format,
                });
            }
            else if (targetCell) {
                if ((clipboardOption === null || clipboardOption === void 0 ? void 0 : clipboardOption.pasteOption) === "onlyValue") {
                    this.dispatch("UPDATE_CELL", { ...target, content: "" });
                }
                else if ((clipboardOption === null || clipboardOption === void 0 ? void 0 : clipboardOption.pasteOption) === "onlyFormat") {
                    this.dispatch("UPDATE_CELL", { ...target, style: null, format: "" });
                }
                else {
                    this.dispatch("CLEAR_CELL", target);
                }
            }
        }
        /**
         * Get the newly updated formula, after applying offsets
         */
        getUpdatedContent(sheetId, cell, offsetX, offsetY, operation) {
            const ranges = this.getters.createAdaptedRanges(cell.dependencies, offsetX, offsetY, sheetId);
            return this.getters.buildFormulaContent(sheetId, cell, ranges);
        }
        /**
         * If the origin position given is the top left of a merge, merge the target
         * position.
         */
        pasteMergeIfExist(origin, target, operation) {
            let { sheetId, col, row } = origin;
            const { col: mainCellColOrigin, row: mainCellRowOrigin } = this.getters.getMainCellPosition(sheetId, col, row);
            if (mainCellColOrigin === col && mainCellRowOrigin === row) {
                const merge = this.getters.getMerge(sheetId, col, row);
                if (!merge) {
                    return;
                }
                if (operation === "CUT") {
                    this.dispatch("REMOVE_MERGE", { sheetId, target: [merge] });
                }
                ({ 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,
                        values: table.filtersValues[i],
                    });
                }
            }
        }
        getClipboardContent() {
            return (this.cells
                .map((cells) => {
                return cells
                    .map((c) => c.cell ? this.getters.getCellText(c.cell, this.getters.shouldShowFormulas()) : "")
                    .join("\t");
            })
                .join("\n") || "\t");
        }
        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);
                }
            }
        }
    }

    /** State of the clipboard when copying/cutting figures */
    class ClipboardFigureState {
        constructor(operation, getters, dispatch) {
            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 };
            const chart = getters.getChart(copiedFigureId);
            if (!chart) {
                throw new Error(`No chart for the given id: ${copiedFigureId}`);
            }
            this.copiedChart = chart.copyInSheetId(this.sheetId);
            this.operation = operation;
        }
        isCutAllowed(target) {
            return 0 /* CommandResult.Success */;
        }
        isPasteAllowed(target, option) {
            if (target.length === 0) {
                return 74 /* CommandResult.EmptyTarget */;
            }
            if ((option === null || option === void 0 ? void 0 : option.pasteOption) !== undefined) {
                return 22 /* CommandResult.WrongFigurePasteOption */;
            }
            return 0 /* CommandResult.Success */;
        }
        /**
         * Paste the clipboard content in the given target
         */
        paste(target) {
            const sheetId = this.getters.getActiveSheetId();
            const position = {
                x: this.getters.getColDimensions(sheetId, target[0].left).start,
                y: this.getters.getRowDimensions(sheetId, target[0].top).start,
            };
            const newChart = this.copiedChart.copyInSheetId(sheetId);
            const newId = new UuidGenerator().smallUuid();
            this.dispatch("CREATE_CHART", {
                id: newId,
                sheetId,
                position,
                size: { height: this.copiedFigure.height, width: this.copiedFigure.width },
                definition: newChart.getDefinition(),
            });
            if (this.operation === "CUT") {
                this.dispatch("DELETE_FIGURE", {
                    sheetId: this.copiedChart.sheetId,
                    id: this.copiedFigure.id,
                });
            }
            this.dispatch("SELECT_FIGURE", { id: newId });
        }
        getClipboardContent() {
            return "\t";
        }
        isColRowDirtyingClipboard(position, dimension) {
            return false;
        }
        drawClipboard(renderingContext) { }
    }

    /** State of the clipboard when copying/cutting from the OS clipboard*/
    class ClipboardOsState extends ClipboardCellsAbstractState {
        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 2 /* CommandResult.WillRemoveExistingMerge */;
            }
            return 0 /* CommandResult.Success */;
        }
        paste(target) {
            const values = this.values;
            const pasteZone = this.getPasteZone(target);
            const { left: activeCol, top: activeRow } = pasteZone;
            const { width, height } = zoneToDimension(pasteZone);
            const sheetId = this.getters.getActiveSheetId();
            this.addMissingDimensions(width, height, 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: values[i][j],
                        sheetId,
                    });
                }
            }
            const zone = {
                left: activeCol,
                top: activeRow,
                right: activeCol + width - 1,
                bottom: activeRow + height - 1,
            };
            this.selection.selectZone({ cell: { col: activeCol, row: activeRow }, zone });
        }
        getClipboardContent() {
            return 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 {
        constructor() {
            super(...arguments);
            this.status = "invisible";
            this._isPaintingFormat = false;
        }
        // ---------------------------------------------------------------------------
        // 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 23 /* CommandResult.EmptyClipboard */;
                    }
                    const pasteOption = cmd.pasteOption || (this._isPaintingFormat ? "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);
                }
                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);
                }
            }
            return 0 /* CommandResult.Success */;
        }
        handle(cmd) {
            var _a, _b, _c, _d;
            switch (cmd.type) {
                case "COPY":
                case "CUT":
                    const zones = this.getters.getSelectedZones();
                    this.state = this.getClipboardState(zones, cmd.type);
                    this.status = "visible";
                    break;
                case "PASTE":
                    if (!this.state) {
                        break;
                    }
                    const pasteOption = cmd.pasteOption || (this._isPaintingFormat ? "onlyFormat" : undefined);
                    this._isPaintingFormat = false;
                    this.state.paste(cmd.target, { pasteOption, shouldPasteCF: true, selectTarget: true });
                    if (this.state.operation === "CUT") {
                        this.state = undefined;
                    }
                    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 (((_a = this.state) === null || _a === void 0 ? void 0 : _a.operation) !== "CUT" || cmd.sheetId !== ((_b = this.state) === null || _b === void 0 ? void 0 : _b.sheetId)) {
                        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 (((_c = this.state) === null || _c === void 0 ? void 0 : _c.operation) !== "CUT" || cmd.sheetId !== ((_d = this.state) === null || _d === void 0 ? void 0 : _d.sheetId)) {
                        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":
                    const state = new ClipboardOsState(cmd.text, this.getters, this.dispatch, this.selection);
                    state.paste(cmd.target);
                    this.status = "invisible";
                    break;
                case "ACTIVATE_PAINT_FORMAT": {
                    const zones = this.getters.getSelectedZones();
                    this.state = this.getClipboardStateForCopyCells(zones, "COPY");
                    this._isPaintingFormat = true;
                    this.status = "visible";
                    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() {
            var _a;
            return ((_a = this.state) === null || _a === void 0 ? void 0 : _a.getClipboardContent()) || "\t";
        }
        isCutOperation() {
            return this.state ? this.state.operation === "CUT" : false;
        }
        isPaintingFormat() {
            return this._isPaintingFormat;
        }
        // ---------------------------------------------------------------------------
        // 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);
        }
    }
    ClipboardPlugin.layers = [2 /* LAYERS.Clipboard */];
    ClipboardPlugin.getters = ["getClipboardContent", "isCutOperation", "isPaintingFormat"];

    /**
     * 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 {
        constructor() {
            super(...arguments);
            this.customColors = new Set();
            this.shouldUpdateColors = false;
        }
        handle(cmd) {
            switch (cmd.type) {
                case "UPDATE_CELL":
                case "UPDATE_CHART":
                case "CREATE_CHART":
                case "ADD_CONDITIONAL_FORMAT":
                    this.shouldUpdateColors = true;
            }
        }
        finalize() {
            if (this.shouldUpdateColors) {
                this.shouldUpdateColors = false;
                for (const color of this.getCustomColors()) {
                    this.tryToAddColor(color);
                }
            }
        }
        getCustomColors() {
            let usedColors = [];
            for (const sheetId of this.getters.getSheetIds()) {
                const cells = Object.values(this.getters.getCells(sheetId));
                usedColors = usedColors.concat(this.getColorsFromCells(cells), 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, ...this.customColors])].filter(isColorValid).map(toHex)),
            ]).filter((color) => !COLOR_PICKER_DEFAULTS.includes(color));
        }
        getColorsFromCells(cells) {
            var _a, _b;
            const colors = new Set();
            for (const cell of cells) {
                if ((_a = cell.style) === null || _a === void 0 ? void 0 : _a.textColor) {
                    colors.add(cell.style.textColor);
                }
                if ((_b = cell.style) === null || _b === void 0 ? void 0 : _b.fillColor) {
                    colors.add(cell.style.fillColor);
                }
            }
            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.customColors.add(formattedColor);
            }
        }
    }
    CustomColorsPlugin.getters = ["getCustomColors"];

    const functionMap = functionRegistry.mapping;
    class EvaluationPlugin extends UIPlugin {
        constructor(getters, state, dispatch, config, selection) {
            super(getters, state, dispatch, config, selection);
            this.isUpToDate = false;
            this.evalContext = config.evalContext;
        }
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        handle(cmd) {
            if (invalidateEvaluationCommands.has(cmd.type)) {
                this.isUpToDate = false;
            }
            switch (cmd.type) {
                case "UPDATE_CELL":
                    if ("content" in cmd || "format" in cmd) {
                        this.isUpToDate = false;
                    }
                    break;
                case "EVALUATE_CELLS":
                    this.evaluate();
                    break;
            }
        }
        finalize() {
            if (!this.isUpToDate) {
                this.evaluate();
            }
        }
        // ---------------------------------------------------------------------------
        // Getters
        // ---------------------------------------------------------------------------
        evaluateFormula(formulaString, sheetId = this.getters.getActiveSheetId()) {
            try {
                const compiledFormula = compile(formulaString);
                const params = this.getCompilationParameters(() => { });
                const ranges = [];
                for (let xc of compiledFormula.dependencies) {
                    ranges.push(this.getters.getRangeFromSheetXC(sheetId, xc));
                }
                return compiledFormula.execute(ranges, ...params).value;
            }
            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
                .getCellsInZone(sheet.id, range.zone)
                .map((cell) => (cell === null || cell === void 0 ? void 0 : 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.getCellsInZone(sheet.id, range.zone).map((cell) => cell === null || cell === void 0 ? void 0 : cell.evaluated.value);
        }
        // ---------------------------------------------------------------------------
        // Evaluator
        // ---------------------------------------------------------------------------
        *getAllCells() {
            // use a generator function to avoid re-building a new object
            for (const sheetId of this.getters.getSheetIds()) {
                const cells = this.getters.getCells(sheetId);
                for (const cellId in cells) {
                    yield cells[cellId];
                }
            }
        }
        evaluate() {
            const compilationParameters = this.getCompilationParameters(computeCell);
            const visited = {};
            for (const cell of this.getAllCells()) {
                computeCell(cell);
            }
            this.isUpToDate = true;
            function handleError(e) {
                if (!(e instanceof Error)) {
                    e = new Error(e);
                }
                const msg = (e === null || e === void 0 ? void 0 : e.errorType) || CellErrorType.GenericError;
                // apply function name
                const __lastFnCalled = compilationParameters[2].__lastFnCalled || "";
                return new EvaluationError(msg, e.message.replace("[[FUNCTION_NAME]]", __lastFnCalled), e.logLevel !== undefined ? e.logLevel : CellErrorLevel.error);
            }
            function computeCell(cell) {
                if (!cell.isFormula()) {
                    return;
                }
                const cellId = cell.id;
                const computedCell = lazy(() => {
                    compilationParameters[2].__originCellXC = () => {
                        // compute the value lazily for performance reasons
                        const position = compilationParameters[2].getters.getCellPosition(cellId);
                        return toXC(position.col, position.row);
                    };
                    if (cellId in visited) {
                        if (visited[cellId] === null) {
                            return new CircularDependencyError();
                        }
                        return cell.evaluated;
                    }
                    visited[cellId] = null;
                    try {
                        const computedCell = cell.compiledFormula.execute(cell.dependencies, ...compilationParameters);
                        visited[cellId] = true;
                        if (Array.isArray(computedCell.value)) {
                            // if a value returns an array (like =A1:A3)
                            throw new Error(_lt("This formula depends on invalid values"));
                        }
                        return { value: computedCell.value, format: cell.format || computedCell.format };
                    }
                    catch (error) {
                        visited[cellId] = true;
                        return handleError(error);
                    }
                });
                cell.assignEvaluation(computedCell);
            }
        }
        /**
         * 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
         */
        getCompilationParameters(computeCell) {
            const evalContext = Object.assign(Object.create(functionMap), this.evalContext, {
                getters: this.getters,
            });
            const getters = this.getters;
            function readCell(range) {
                let cell;
                if (!getters.tryGetSheet(range.sheetId)) {
                    throw new Error(_lt("Invalid sheet name"));
                }
                cell = getters.getCell(range.sheetId, range.zone.left, range.zone.top);
                if (!cell || cell.isEmpty()) {
                    // magic "empty" value
                    // Returning {value: null} instead of undefined will ensure that we don't
                    // fall back on the default value of the argument provided to the formula's compute function
                    return { value: null, format: cell === null || cell === void 0 ? void 0 : cell.format };
                }
                return getEvaluatedCell(cell);
            }
            function getEvaluatedCell(cell) {
                if (cell.evaluated.type === CellValueType.error) {
                    throw new EvaluationError(cell.evaluated.value, cell.evaluated.error.message, cell.evaluated.error.logLevel);
                }
                return cell.evaluated;
            }
            const rangeCache = {};
            /**
             * 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.
             */
            function range(range) {
                assertRangeValid(range);
                const sheetId = range.sheetId;
                // 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 = getters.getSheetZone(sheetId);
                const zone = intersection(range.zone, sheetZone);
                if (!zone) {
                    return [[]];
                }
                const { top, left, bottom, right } = zone;
                const cacheKey = `${sheetId}-${top}-${left}-${bottom}-${right}`;
                if (cacheKey in rangeCache) {
                    return rangeCache[cacheKey];
                }
                const result = new Array(right - left + 1);
                // Performance issue: nested loop is faster than a map here
                for (let col = left; col <= right; col++) {
                    const rowValues = new Array(bottom - top + 1);
                    for (let row = top; row <= bottom; row++) {
                        const cell = evalContext.getters.getCell(range.sheetId, col, row);
                        rowValues[row - top] = cell ? getEvaluatedCell(cell) : undefined;
                    }
                    result[col - left] = rowValues;
                }
                rangeCache[cacheKey] = result;
                return result;
            }
            /**
             * 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
             */
            function refFn(range, isMeta, functionName, paramNumber) {
                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
                        ? _lt("Function %s expects the parameter %s to be a single value or a single cell reference, not a range.", functionName.toString(), paramNumber.toString())
                        : _lt("Function %s expects its parameters to be single values or single cell references, not ranges.", functionName.toString()));
                }
                return readCell(range);
            }
            function assertRangeValid(range) {
                if (!isZoneValid(range.zone)) {
                    throw new InvalidReferenceError();
                }
                if (range.invalidSheetName) {
                    throw new Error(_lt("Invalid sheet name: %s", range.invalidSheetName));
                }
            }
            return [refFn, range, evalContext];
        }
    }
    EvaluationPlugin.getters = ["evaluateFormula", "getRangeFormattedValues", "getRangeValues"];

    class EvaluationChartPlugin extends UIPlugin {
        constructor() {
            super(...arguments);
            this.charts = {};
            this.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),
            };
        }
    }
    EvaluationChartPlugin.getters = ["getChartRuntime", "getStyleOfSingleCellChart"];

    // -----------------------------------------------------------------------------
    // Constants
    // -----------------------------------------------------------------------------
    class EvaluationConditionalFormatPlugin extends UIPlugin {
        constructor() {
            super(...arguments);
            this.isStale = true;
            // stores the computed styles in the format of computedStyles.sheetName[col][row] = Style
            this.computedStyles = {};
            this.computedIcons = {};
            this.uuidGenerator = new UuidGenerator();
            /**
             * Execute the predicate to know if a conditional formatting rule should be applied to a cell
             */
            this.rulePredicate = {
                CellIsRule: (cell, rule) => {
                    if (cell && cell.evaluated.type === CellValueType.error) {
                        return false;
                    }
                    const values = rule.values.map(parsePrimitiveContent);
                    switch (rule.operator) {
                        case "IsEmpty":
                            return !isDefined$1(cell) || cell.evaluated.value.toString().trim() === "";
                        case "IsNotEmpty":
                            return isDefined$1(cell) && cell.evaluated.value.toString().trim() !== "";
                        case "BeginsWith":
                            if (!cell && values[0] === "") {
                                return false;
                            }
                            return (isDefined$1(cell) && (cell === null || cell === void 0 ? void 0 : cell.evaluated.value.toString().startsWith(values[0].toString())));
                        case "EndsWith":
                            if (!cell && values[0] === "") {
                                return false;
                            }
                            return isDefined$1(cell) && cell.evaluated.value.toString().endsWith(values[0].toString());
                        case "Between":
                            return (isDefined$1(cell) &&
                                cell.evaluated.value >= values[0] &&
                                cell.evaluated.value <= values[1]);
                        case "NotBetween":
                            return !(isDefined$1(cell) &&
                                cell.evaluated.value >= values[0] &&
                                cell.evaluated.value <= values[1]);
                        case "ContainsText":
                            return (isDefined$1(cell) && cell.evaluated.value.toString().indexOf(values[0].toString()) > -1);
                        case "NotContains":
                            return (!isDefined$1(cell) ||
                                !cell.evaluated.value ||
                                cell.evaluated.value.toString().indexOf(values[0].toString()) == -1);
                        case "GreaterThan":
                            return isDefined$1(cell) && cell.evaluated.value > values[0];
                        case "GreaterThanOrEqual":
                            return isDefined$1(cell) && cell.evaluated.value >= values[0];
                        case "LessThan":
                            return isDefined$1(cell) && cell.evaluated.value < values[0];
                        case "LessThanOrEqual":
                            return isDefined$1(cell) && cell.evaluated.value <= values[0];
                        case "NotEqual":
                            if (!isDefined$1(cell) && values[0] === "") {
                                return false;
                            }
                            return isDefined$1(cell) && cell.evaluated.value !== values[0];
                        case "Equal":
                            if (!cell && values[0] === "") {
                                return true;
                            }
                            return isDefined$1(cell) && cell.evaluated.value === values[0];
                        default:
                            console.warn(_lt("Not implemented operator %s for kind of conditional formatting:  %s", rule.operator, rule.type));
                    }
                    return false;
                },
            };
        }
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        handle(cmd) {
            if (invalidateCFEvaluationCommands.has(cmd.type) ||
                (cmd.type === "UPDATE_CELL" && "content" in cmd)) {
                this.isStale = true;
            }
            switch (cmd.type) {
                case "ACTIVATE_SHEET":
                    const activeSheet = cmd.sheetIdTo;
                    this.computedStyles[activeSheet] = this.computedStyles[activeSheet] || {};
                    this.computedIcons[activeSheet] = this.computedIcons[activeSheet] || {};
                    this.isStale = true;
                    break;
                case "AUTOFILL_CELL":
                    const sheetId = this.getters.getActiveSheetId();
                    const cfOrigin = this.getters.getRulesByCell(sheetId, cmd.originCol, cmd.originRow);
                    for (const cf of cfOrigin) {
                        this.adaptRules(sheetId, cf, [toXC(cmd.col, cmd.row)], []);
                    }
                    break;
                case "PASTE_CONDITIONAL_FORMAT":
                    this.pasteCf(cmd.origin, cmd.target, cmd.operation);
                    break;
            }
        }
        finalize() {
            if (this.isStale) {
                this.computeStyles();
                this.isStale = false;
            }
        }
        // ---------------------------------------------------------------------------
        // Getters
        // ---------------------------------------------------------------------------
        getCellComputedStyle(sheetId, col, row) {
            var _a;
            const cell = this.getters.getCell(sheetId, col, row);
            const styles = this.computedStyles[sheetId];
            const cfStyle = styles && ((_a = styles[col]) === null || _a === void 0 ? void 0 : _a[row]);
            const computedStyle = {
                ...cell === null || cell === void 0 ? void 0 : cell.style,
                ...cfStyle,
            };
            if (this.getters.isFilterHeader(sheetId, col, row)) {
                computedStyle.bold = true;
            }
            return computedStyle;
        }
        getConditionalIcon(col, row) {
            var _a;
            const activeSheet = this.getters.getActiveSheetId();
            const icon = this.computedIcons[activeSheet];
            return icon && ((_a = icon[col]) === null || _a === void 0 ? void 0 : _a[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
         */
        computeStyles() {
            var _a;
            const activeSheetId = this.getters.getActiveSheetId();
            this.computedStyles[activeSheetId] = {};
            this.computedIcons[activeSheetId] = {};
            const computedStyle = this.computedStyles[activeSheetId];
            for (let cf of this.getters.getConditionalFormats(activeSheetId).reverse()) {
                switch (cf.rule.type) {
                    case "ColorScaleRule":
                        for (let range of cf.ranges) {
                            this.applyColorScale(range, cf.rule);
                        }
                        break;
                    case "IconSetRule":
                        for (let range of cf.ranges) {
                            this.applyIcon(range, cf.rule);
                        }
                        break;
                    default:
                        for (let ref of cf.ranges) {
                            const zone = this.getters.getRangeFromSheetXC(activeSheetId, ref).zone;
                            for (let row = zone.top; row <= zone.bottom; row++) {
                                for (let col = zone.left; col <= zone.right; col++) {
                                    const pr = this.rulePredicate[cf.rule.type];
                                    let cell = this.getters.getCell(activeSheetId, col, row);
                                    if (pr && pr(cell, cf.rule)) {
                                        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(((_a = computedStyle[col]) === null || _a === void 0 ? void 0 : _a[row]) || {}, cf.rule.style);
                                    }
                                }
                            }
                        }
                        break;
                }
            }
        }
        parsePoint(range, threshold, functionName) {
            const sheetId = this.getters.getActiveSheetId();
            const rangeValues = this.getters
                .getRangeValues(this.getters.getRangeFromSheetXC(sheetId, range))
                .filter(this.isCellValueNumber);
            switch (threshold.type) {
                case "value":
                    const result = functionName === "max" ? largeMax(rangeValues) : largeMin(rangeValues);
                    return result;
                case "number":
                    return Number(threshold.value);
                case "percentage":
                    const min = largeMin(rangeValues);
                    const max = largeMax(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(threshold.value);
                    return typeof value === "number" ? value : null;
                default:
                    return null;
            }
        }
        applyIcon(range, rule) {
            const lowerInflectionPoint = this.parsePoint(range, rule.lowerInflectionPoint);
            const upperInflectionPoint = this.parsePoint(range, rule.upperInflectionPoint);
            if (lowerInflectionPoint === null ||
                upperInflectionPoint === null ||
                lowerInflectionPoint > upperInflectionPoint) {
                return;
            }
            const activeSheetId = this.getters.getActiveSheetId();
            const zone = this.getters.getRangeFromSheetXC(activeSheetId, range).zone;
            const computedIcons = this.computedIcons[activeSheetId];
            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.getCell(activeSheetId, col, row);
                    if ((cell === null || cell === void 0 ? void 0 : cell.evaluated.type) !== CellValueType.number) {
                        continue;
                    }
                    const icon = this.computeIcon(cell.evaluated.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];
        }
        applyColorScale(range, rule) {
            var _a;
            const minValue = this.parsePoint(range, rule.minimum, "min");
            const midValue = rule.midpoint ? this.parsePoint(range, rule.midpoint) : null;
            const maxValue = this.parsePoint(range, rule.maximum, "max");
            if (minValue === null ||
                maxValue === null ||
                minValue >= maxValue ||
                (midValue && (minValue >= midValue || midValue >= maxValue))) {
                return;
            }
            const activeSheetId = this.getters.getActiveSheetId();
            const zone = this.getters.getRangeFromSheetXC(activeSheetId, range).zone;
            const computedStyle = this.computedStyles[activeSheetId];
            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.getCell(activeSheetId, col, row);
                    if ((cell === null || cell === void 0 ? void 0 : cell.evaluated.type) === CellValueType.number) {
                        const value = clip(cell.evaluated.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] = ((_a = computedStyle[col]) === null || _a === void 0 ? void 0 : _a[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;
        }
        /**
         * Add or remove cells to a given conditional formatting rule.
         */
        adaptRules(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);
            const newRangesXC = recomputeZones(currentRanges, toRemove);
            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,
            });
        }
        pasteCf(origin, target, operation) {
            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 (operation === "CUT") {
                            //remove from current rule
                            toRemoveRange.push(toXC(origin.col, origin.row));
                        }
                        if (origin.sheetId === target.sheetId) {
                            this.adaptRules(origin.sheetId, cf, [xc], toRemoveRange);
                        }
                        else {
                            this.adaptRules(origin.sheetId, cf, [], toRemoveRange);
                            const cfToCopyTo = this.getCFToCopyTo(target.sheetId, cf);
                            this.adaptRules(target.sheetId, cfToCopyTo, [xc], []);
                        }
                    }
                }
            }
        }
        isCellValueNumber(value) {
            return typeof value === "number";
        }
        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.smallUuid(), ranges: [] };
        }
    }
    EvaluationConditionalFormatPlugin.getters = ["getConditionalIcon", "getCellComputedStyle"];

    class FilterEvaluationPlugin extends UIPlugin {
        constructor() {
            super(...arguments);
            this.filterValues = {};
            this.hiddenRows = new Set();
            this.isEvaluationDirty = false;
        }
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "UPDATE_FILTER":
                    if (!this.getters.getFilterId(cmd.sheetId, cmd.col, cmd.row)) {
                        return 80 /* CommandResult.FilterNotFound */;
                    }
                    break;
            }
            return 0 /* CommandResult.Success */;
        }
        handle(cmd) {
            switch (cmd.type) {
                case "UNDO":
                case "REDO":
                case "UPDATE_CELL":
                case "EVALUATE_CELLS":
                case "ACTIVATE_SHEET":
                case "REMOVE_FILTER_TABLE":
                    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 "ADD_COLUMNS_ROWS":
                case "REMOVE_COLUMNS_ROWS":
                    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(cmd.sheetId, zone.left, 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(sheetId, col, row) {
            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(sheetId, col, row);
            // Use removeFalsyAttributes to avoid overwriting filter borders with undefined values
            const border = { ...filterBorder, ...removeFalsyAttributes(cellBorder || {}) };
            return isObjectEmptyRecursive(border) ? null : border;
        }
        getFilterHeaders(sheetId) {
            const headers = [];
            for (let filters of this.getters.getFilterTables(sheetId)) {
                const zone = filters.zone;
                if (!zone) {
                    continue;
                }
                const row = zone.top;
                for (let col = zone.left; col <= zone.right; col++) {
                    if (this.getters.isColHidden(sheetId, col) || this.getters.isRowHidden(sheetId, row)) {
                        continue;
                    }
                    headers.push({ col, row });
                }
            }
            return headers;
        }
        getFilterValues(sheetId, col, row) {
            const id = this.getters.getFilterId(sheetId, col, row);
            if (!id || !this.filterValues[sheetId])
                return [];
            return this.filterValues[sheetId][id] || [];
        }
        isFilterHeader(sheetId, col, row) {
            const headers = this.getFilterHeaders(sheetId);
            return headers.some((header) => header.col === col && header.row === row);
        }
        isFilterActive(sheetId, col, row) {
            var _a, _b;
            const id = this.getters.getFilterId(sheetId, col, row);
            return Boolean(id && ((_b = (_a = this.filterValues[sheetId]) === null || _a === void 0 ? void 0 : _a[id]) === null || _b === void 0 ? void 0 : _b.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, values, sheetId }) {
            const id = this.getters.getFilterId(sheetId, col, row);
            if (!id)
                return;
            if (!this.filterValues[sheetId])
                this.filterValues[sheetId] = {};
            this.filterValues[sheetId][id] = values;
        }
        updateHiddenRows() {
            var _a, _b;
            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 (this.getters.isRowHiddenByUser(sheetId, filter.zoneWithHeaders.top))
                    continue;
                if (hiddenRows.has(filter.zoneWithHeaders.top))
                    continue;
                const filteredValues = (_b = (_a = this.filterValues[sheetId]) === null || _a === void 0 ? void 0 : _a[filter.id]) === null || _b === void 0 ? void 0 : _b.map(toLowerCase);
                if (!filteredValues || !filter.filteredZone)
                    continue;
                const filteredValuesSet = new Set(filteredValues);
                for (let row = filter.filteredZone.top; row <= filter.filteredZone.bottom; row++) {
                    const value = this.getCellValueAsString(sheetId, filter.col, row);
                    if (filteredValuesSet.has(value)) {
                        hiddenRows.add(row);
                    }
                }
            }
            this.hiddenRows = hiddenRows;
        }
        getCellValueAsString(sheetId, col, row) {
            var _a;
            const value = (_a = this.getters.getCell(sheetId, col, row)) === null || _a === void 0 ? void 0 : _a.formattedValue;
            return (value === null || value === void 0 ? void 0 : value.toLowerCase()) || "";
        }
        exportForExcel(data) {
            var _a;
            for (const sheetData of data.sheets) {
                for (const tableData of sheetData.filterTables) {
                    const tableZone = toZone(tableData.range);
                    const filters = [];
                    const headerNames = [];
                    for (const i of range(0, zoneToDimension(tableZone).width)) {
                        const filteredValues = this.getFilterValues(sheetData.id, tableZone.left + i, tableZone.top);
                        const filter = this.getters.getFilter(sheetData.id, tableZone.left + i, tableZone.top);
                        if (!filter)
                            continue;
                        const valuesInFilterZone = filter.filteredZone
                            ? positions(filter.filteredZone).map((pos) => { var _a; return (_a = this.getters.getCell(sheetData.id, pos.col, pos.row)) === null || _a === void 0 ? void 0 : _a.formattedValue; })
                            : [];
                        if (filteredValues.length) {
                            const xlsxDisplayedValues = valuesInFilterZone
                                .filter(isNonEmptyString)
                                .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 };
                        const headerString = (_a = this.getters.getCell(sheetData.id, headerPosition.col, headerPosition.row)) === null || _a === void 0 ? void 0 : _a.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;
        }
    }
    FilterEvaluationPlugin.getters = [
        "getCellBorderWithFilterBorder",
        "getFilterHeaders",
        "getFilterValues",
        "isFilterHeader",
        "isRowFiltered",
        "isFilterActive",
    ];

    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 {
        constructor() {
            super(...arguments);
            this.searchMatches = [];
            this.selectedMatchIndex = null;
            this.currentSearchRegex = null;
            this.searchOptions = {
                matchCase: false,
                exactMatch: false,
                searchFormulas: false,
            };
            this.toSearch = "";
            this.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 "EVALUATE_CELLS":
                case "UPDATE_CELL":
                case "REMOVE_FILTER_TABLE":
                case "UPDATE_FILTER":
                    this.isSearchDirty = true;
                    break;
                case "UNDO":
                case "REDO":
                case "REMOVE_COLUMNS_ROWS":
                case "HIDE_COLUMNS_ROWS":
                case "UNHIDE_COLUMNS_ROWS":
                case "ADD_COLUMNS_ROWS":
                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) {
                for (const cell of Object.values(cells)) {
                    const { col, row } = this.getters.getCellPosition(cell.id);
                    const isColHidden = this.getters.isColHidden(sheetId, col);
                    const isRowHidden = this.getters.isRowHidden(sheetId, row);
                    if (isColHidden || isRowHidden) {
                        continue;
                    }
                    if (cell &&
                        this.currentSearchRegex &&
                        this.currentSearchRegex.test(this.searchOptions.searchFormulas ? cell.content : String(cell.evaluated.value))) {
                        const position = this.getters.getCellPosition(cell.id);
                        const match = { col: position.col, row: position.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;
            if (this.selectedMatchIndex === null || this.selectedMatchIndex !== nextIndex) {
                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
        // ---------------------------------------------------------------------------
        /**
         * Replace the value of the currently selected match
         */
        replace(replaceWith) {
            if (this.selectedMatchIndex === null || !this.currentSearchRegex) {
                return;
            }
            const matches = this.searchMatches;
            const selectedMatch = matches[this.selectedMatchIndex];
            const sheetId = this.getters.getActiveSheetId();
            const cellToReplace = this.getters.getCell(sheetId, selectedMatch.col, selectedMatch.row);
            const toReplace = this.toReplace(cellToReplace, sheetId);
            if (!cellToReplace || !toReplace) {
                this.selectNextCell(Direction.next);
            }
            else {
                const replaceRegex = new RegExp(this.currentSearchRegex.source, this.currentSearchRegex.flags + "g");
                const newContent = toReplace.toString().replace(replaceRegex, replaceWith);
                this.dispatch("UPDATE_CELL", {
                    sheetId: this.getters.getActiveSheetId(),
                    col: selectedMatch.col,
                    row: selectedMatch.row,
                    content: newContent,
                });
                this.searchMatches.splice(this.selectedMatchIndex, 1);
                this.selectNextCell(Direction.current);
            }
        }
        /**
         * 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.replace(replaceWith);
            }
        }
        /**
         * Determines if the content, the value or nothing should be replaced,
         * based on the search and replace options
         */
        toReplace(cell, sheetId) {
            if (cell) {
                if (this.searchOptions.searchFormulas && cell.isFormula()) {
                    return cell.content;
                }
                else if (this.searchOptions.searchFormulas || !cell.isFormula()) {
                    return cell.evaluated.value.toString();
                }
            }
            return null;
        }
        // ---------------------------------------------------------------------------
        // Grid rendering
        // ---------------------------------------------------------------------------
        drawGrid(renderingContext) {
            const { ctx } = renderingContext;
            const sheetId = this.getters.getActiveSheetId();
            for (const match of this.searchMatches) {
                const merge = this.getters.getMerge(sheetId, match.col, 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);
                    }
                }
            }
        }
    }
    FindAndReplacePlugin.layers = [3 /* LAYERS.Search */];
    FindAndReplacePlugin.getters = ["getSearchMatches", "getCurrentSelectedMatchIndex"];

    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, the format resulting from the change of the
         * first cell (with number type) will be applied to the whole zone.
         */
        setDecimal(sheetId, zones, step) {
            // Find the first cell with a number value and get the format
            const numberFormat = this.searchNumberFormat(sheetId, zones);
            if (numberFormat !== undefined) {
                // Depending on the step sign, increase or decrease the decimal representation
                // of the format
                const newFormat = changeDecimalPlaces(numberFormat, step);
                // Apply the new format on the whole zone
                this.dispatch("SET_FORMATTING", {
                    sheetId,
                    target: zones,
                    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.
         */
        searchNumberFormat(sheetId, zones) {
            var _a;
            for (let zone of zones) {
                for (let row = zone.top; row <= zone.bottom; row++) {
                    for (let col = zone.left; col <= zone.right; col++) {
                        const cell = this.getters.getCell(sheetId, col, row);
                        if ((cell === null || cell === void 0 ? void 0 : cell.evaluated.type) === CellValueType.number &&
                            !((_a = cell.evaluated.format) === null || _a === void 0 ? void 0 : _a.match(DATETIME_FORMAT)) // reject dates
                        ) {
                            return cell.evaluated.format || createDefaultFormat(cell.evaluated.value);
                        }
                    }
                }
            }
            return undefined;
        }
    }
    FormatPlugin.modes = ["normal"];

    class HeaderVisibilityUIPlugin extends UIPlugin {
        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 {
                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, indexes) {
            let lastIndex;
            for (lastIndex = indexes.last; lastIndex >= indexes.first; lastIndex--) {
                if (!this.isHeaderHidden(sheetId, dimension, lastIndex)) {
                    return lastIndex;
                }
            }
            return lastIndex;
        }
        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;
                }
            }
        }
    }
    HeaderVisibilityUIPlugin.getters = [
        "getNextVisibleCellPosition",
        "findVisibleHeader",
        "findLastVisibleColRowIndex",
        "findFirstVisibleColRowIndex",
        "isRowHidden",
        "isColHidden",
        "isHeaderHidden",
    ];

    /**
     * HighlightPlugin
     */
    class HighlightPlugin extends UIPlugin {
        // ---------------------------------------------------------------------------
        // 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 { height, width } = zoneToDimension(highlight.zone);
                const zone = height * width === 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);
                }
            }
        }
    }
    HighlightPlugin.layers = [1 /* LAYERS.Highlights */];
    HighlightPlugin.getters = ["getHighlights"];

    class RendererPlugin extends UIPlugin {
        constructor() {
            super(...arguments);
            this.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.drawBackground(renderingContext);
                    for (const zone of this.getters.getAllActiveViewportsZones()) {
                        const { ctx } = renderingContext;
                        ctx.save();
                        ctx.beginPath();
                        const rect = this.getters.getVisibleRect(zone);
                        ctx.rect(rect.x, rect.y, rect.width, rect.height);
                        ctx.clip();
                        this.boxes = this.getGridBoxes(zone);
                        this.drawCellBackground(renderingContext);
                        this.drawBorders(renderingContext);
                        this.drawTexts(renderingContext);
                        this.drawIcon(renderingContext);
                        ctx.restore();
                    }
                    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();
            const sheetId = this.getters.getActiveSheetId();
            // white background
            ctx.fillStyle = "#ffffff";
            ctx.fillRect(0, 0, width + CANVAS_SHIFT, height + CANVAS_SHIFT);
            // background grid
            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];
            if (!this.getters.getGridLinesVisibility(sheetId) || this.getters.isDashboard()) {
                return;
            }
            ctx.lineWidth = 2 * thinLineWidth;
            ctx.strokeStyle = CELL_BORDER_COLOR;
            ctx.beginPath();
            // vertical lines
            for (const i of visibleCols) {
                const zone = { top, bottom, left: i, right: i };
                const { x, width: colWidth, height: colHeight } = this.getters.getVisibleRect(zone);
                ctx.moveTo(x + colWidth, 0);
                ctx.lineTo(x + colWidth, Math.min(height, colHeight + (this.getters.isDashboard() ? 0 : HEADER_HEIGHT)));
            }
            // horizontal lines
            for (const i of visibleRows) {
                const zone = { left, right, top: i, bottom: i };
                const { y, width: rowWidth, height: rowHeight } = this.getters.getVisibleRect(zone);
                ctx.moveTo(0, y + rowHeight);
                ctx.lineTo(Math.min(width, rowWidth + (this.getters.isDashboard() ? 0 : HEADER_WIDTH)), y + rowHeight);
            }
            ctx.stroke();
        }
        drawCellBackground(renderingContext) {
            const { ctx, thinLineWidth } = renderingContext;
            const areGridLinesVisible = !this.getters.isDashboard() &&
                this.getters.getGridLinesVisibility(this.getters.getActiveSheetId());
            ctx.lineWidth = areGridLinesVisible ? 0.3 * thinLineWidth : thinLineWidth;
            const inset = areGridLinesVisible ? 0.1 * thinLineWidth : 0;
            ctx.strokeStyle = "#111";
            for (let box of this.boxes) {
                // fill color
                let style = box.style;
                if ((style.fillColor && style.fillColor !== "#ffffff") || box.isMerge) {
                    ctx.fillStyle = style.fillColor || "#ffffff";
                    if (areGridLinesVisible) {
                        ctx.fillRect(box.x, box.y, box.width, box.height);
                        ctx.strokeRect(box.x + inset, box.y + inset, box.width - 2 * inset, box.height - 2 * inset);
                    }
                    else {
                        ctx.fillRect(box.x - thinLineWidth, box.y - thinLineWidth, box.width + 2 * thinLineWidth, box.height + 2 * thinLineWidth);
                    }
                }
                if (box.error) {
                    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();
                }
            }
        }
        drawBorders(renderingContext) {
            const { ctx, thinLineWidth } = 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);
                    }
                }
            }
            function drawBorder([style, color], x1, y1, x2, y2) {
                ctx.strokeStyle = color;
                ctx.lineWidth = (style === "thin" ? 2 : 3) * thinLineWidth;
                ctx.beginPath();
                ctx.moveTo(x1, y1);
                ctx.lineTo(x2, y2);
                ctx.stroke();
            }
        }
        drawTexts(renderingContext) {
            const { ctx, thinLineWidth } = 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";
                    const font = computeTextFont(style);
                    if (font !== currentFont) {
                        currentFont = font;
                        ctx.font = font;
                    }
                    ctx.fillStyle = style.textColor || "#000";
                    let x;
                    let y = box.y + box.height / 2 + 1;
                    if (align === "left") {
                        x = box.x + (box.image ? box.image.size + 2 * MIN_CF_ICON_MARGIN : MIN_CELL_TEXT_MARGIN);
                    }
                    else if (align === "right") {
                        x =
                            box.x +
                                box.width -
                                MIN_CELL_TEXT_MARGIN -
                                (box.isFilterHeader ? ICON_EDGE_LENGTH + FILTER_ICON_MARGIN : 0);
                    }
                    else {
                        x = box.x + box.width / 2;
                    }
                    ctx.textAlign = align;
                    if (box.clipRect) {
                        ctx.save();
                        ctx.beginPath();
                        const { x, y, width, height } = box.clipRect;
                        ctx.rect(x, y, width, height);
                        ctx.clip();
                    }
                    const brokenLineNumber = box.content.multiLineText.length;
                    const size = computeTextFontSizeInPixels(style);
                    const contentHeight = brokenLineNumber * (size + MIN_CELL_TEXT_MARGIN) - MIN_CELL_TEXT_MARGIN;
                    let brokenLineY = y - contentHeight / 2;
                    for (let brokenLine of box.content.multiLineText) {
                        ctx.fillText(brokenLine, Math.round(x), Math.round(brokenLineY));
                        if (style.strikethrough || style.underline) {
                            const lineWidth = computeTextWidth(ctx, brokenLine, style);
                            let _x = x;
                            if (align === "right") {
                                _x -= lineWidth;
                            }
                            else if (align === "center") {
                                _x -= lineWidth / 2;
                            }
                            if (style.strikethrough) {
                                ctx.fillRect(_x, brokenLineY + size / 2, lineWidth, 2.6 * thinLineWidth);
                            }
                            if (style.underline) {
                                ctx.fillRect(_x, brokenLineY + size + 1, lineWidth, 1.3 * thinLineWidth);
                            }
                        }
                        brokenLineY += MIN_CELL_TEXT_MARGIN + size;
                    }
                    if (box.clipRect) {
                        ctx.restore();
                    }
                }
            }
        }
        drawIcon(renderingContext) {
            const { ctx } = renderingContext;
            for (const box of this.boxes) {
                if (box.image) {
                    const icon = box.image.image;
                    const size = box.image.size;
                    const margin = (box.height - size) / 2;
                    if (box.image.clipIcon) {
                        ctx.save();
                        ctx.beginPath();
                        const { x, y, width, height } = box.image.clipIcon;
                        ctx.rect(x, y, width, height);
                        ctx.clip();
                    }
                    ctx.drawImage(icon, box.x + MIN_CF_ICON_MARGIN, box.y + margin, size, size);
                    if (box.image.clipIcon) {
                        ctx.restore();
                    }
                }
            }
        }
        drawHeaders(renderingContext) {
            const { ctx, thinLineWidth } = renderingContext;
            const visibleCols = this.getters.getSheetViewVisibleCols();
            const left = visibleCols[0];
            const visibleRows = this.getters.getSheetViewVisibleRows();
            const top = visibleRows[0];
            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 (const col of visibleCols) {
                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 (const row of visibleRows) {
                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 col of visibleCols) {
                const colSize = this.getters.getColSize(sheetId, col);
                const colName = numberToLetters(col);
                ctx.fillStyle = activeCols.has(col) ? "#fff" : TEXT_HEADER_COLOR;
                let colStart = this.getHeaderOffset("COL", left, col);
                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 row of visibleRows) {
                const rowSize = this.getters.getRowSize(sheetId, row);
                ctx.fillStyle = activeRows.has(row) ? "#fff" : TEXT_HEADER_COLOR;
                let rowStart = this.getHeaderOffset("ROW", top, row);
                ctx.fillText(String(row + 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 = "#BCBCBC";
            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 = "#DADFE8";
            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();
        }
        hasContent(col, row) {
            const sheetId = this.getters.getActiveSheetId();
            const cell = this.getters.getCell(sheetId, col, row);
            return (cell && !cell.isEmpty()) || this.getters.isInMerge(sheetId, col, row);
        }
        findNextEmptyCol(base, max, row) {
            let col = base;
            while (col < max && !this.hasContent(col + 1, row)) {
                col++;
            }
            return col;
        }
        findPreviousEmptyCol(base, min, row) {
            let col = base;
            while (col > min && !this.hasContent(col - 1, row)) {
                col--;
            }
            return col;
        }
        computeCellAlignment(cell, isOverflowing) {
            if (cell.isFormula() && this.getters.shouldShowFormulas()) {
                return "left";
            }
            const { align } = this.getters.getCellStyle(cell);
            if (isOverflowing && cell.evaluated.type === CellValueType.number) {
                return align !== "center" ? "left" : align;
            }
            return align || cell.defaultAlign;
        }
        createZoneBox(sheetId, zone, viewport) {
            const { left, right } = viewport;
            const col = zone.left;
            const row = zone.top;
            const cell = this.getters.getCell(sheetId, col, row);
            const showFormula = this.getters.shouldShowFormulas();
            const { x, y, width, height } = this.getters.getRect(zone);
            const box = {
                x,
                y,
                width,
                height,
                border: this.getters.getCellBorderWithFilterBorder(sheetId, col, row) || undefined,
                style: this.getters.getCellComputedStyle(sheetId, col, row),
            };
            if (!cell) {
                return box;
            }
            /** Icon CF */
            const cfIcon = this.getters.getConditionalIcon(col, row);
            const fontSizePX = computeTextFontSizeInPixels(box.style);
            const iconBoxWidth = cfIcon ? 2 * 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 */
            box.isFilterHeader = this.getters.isFilterHeader(sheetId, col, row);
            const headerIconWidth = box.isFilterHeader ? FILTER_ICON_EDGE_LENGTH + FILTER_ICON_MARGIN : 0;
            /** Content */
            const text = this.getters.getCellText(cell, showFormula);
            const textWidth = this.getters.getTextWidth(cell);
            const wrapping = this.getters.getCellStyle(cell).wrapping || "overflow";
            const multiLineText = wrapping === "wrap"
                ? this.getters.getCellMultiLineText(cell, width - 2 * MIN_CELL_TEXT_MARGIN)
                : [text];
            const contentWidth = iconBoxWidth + textWidth + headerIconWidth;
            const align = this.computeCellAlignment(cell, contentWidth > width);
            box.content = {
                multiLineText,
                width: wrapping === "overflow" ? textWidth : width,
                align,
            };
            /** Error */
            if (cell.evaluated.type === CellValueType.error &&
                cell.evaluated.error.logLevel > CellErrorLevel.silent) {
                box.error = cell.evaluated.error.message;
            }
            /** ClipRect */
            const isOverflowing = contentWidth > width || fontSizePX > height;
            if (cfIcon || box.isFilterHeader) {
                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(sheetId, col, row);
                if (isCellInMerge) {
                    // Always clip merges
                    nextColIndex = this.getters.getMerge(sheetId, col, row).right;
                    previousColIndex = col;
                }
                else {
                    nextColIndex = this.findNextEmptyCol(col, right, row);
                    previousColIndex = this.findPreviousEmptyCol(col, left, row);
                }
                switch (align) {
                    case "left": {
                        const emptyZoneOnTheLeft = positionToZone({ col: nextColIndex, row });
                        const { x, y, width, height } = this.getters.getVisibleRect(union(zone, emptyZoneOnTheLeft));
                        if (width < textWidth || fontSizePX > height) {
                            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 < textWidth || fontSizePX > height) {
                            box.clipRect = { x, y, width, height };
                        }
                        break;
                    }
                    case "center": {
                        const emptyZone = {
                            ...zone,
                            right: nextColIndex,
                            left: previousColIndex,
                        };
                        const { x, y, width, height } = this.getters.getVisibleRect(emptyZone);
                        if (width < textWidth ||
                            previousColIndex === col ||
                            nextColIndex === col ||
                            fontSizePX > height) {
                            box.clipRect = { x, y, width, height };
                        }
                        break;
                    }
                }
            }
            else if (wrapping === "clip" || wrapping === "wrap") {
                box.clipRect = {
                    x: box.x,
                    y: box.y,
                    width,
                    height,
                };
            }
            return box;
        }
        getGridBoxes(zone) {
            const boxes = [];
            const visibleCols = this.getters
                .getSheetViewVisibleCols()
                .filter((col) => col >= zone.left && col <= zone.right);
            const left = visibleCols[0];
            const right = visibleCols[visibleCols.length - 1];
            const visibleRows = this.getters
                .getSheetViewVisibleRows()
                .filter((row) => row >= zone.top && row <= zone.bottom);
            const top = visibleRows[0];
            const bottom = visibleRows[visibleRows.length - 1];
            const viewport = { left, right, top, bottom };
            const sheetId = this.getters.getActiveSheetId();
            for (const rowNumber of visibleRows) {
                for (const colNumber of visibleCols) {
                    if (this.getters.isInMerge(sheetId, colNumber, rowNumber)) {
                        continue;
                    }
                    boxes.push(this.createZoneBox(sheetId, positionToZone({ col: colNumber, row: rowNumber }), 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, merge.right, merge.bottom);
                    box.border = {
                        ...box.border,
                        bottom: borderBottomRight ? borderBottomRight.bottom : undefined,
                        right: borderBottomRight ? borderBottomRight.right : undefined,
                    };
                    box.isMerge = true;
                    boxes.push(box);
                }
            }
            return boxes;
        }
    }
    RendererPlugin.layers = [0 /* LAYERS.Background */, 7 /* LAYERS.Headers */];
    RendererPlugin.getters = ["getColDimensionsInViewport", "getRowDimensionsInViewport"];

    const selectionStatisticFunctions = [
        {
            name: _lt("Sum"),
            types: [CellValueType.number],
            compute: (values) => SUM.compute([values]),
        },
        {
            name: _lt("Avg"),
            types: [CellValueType.number],
            compute: (values) => AVERAGE.compute([values]),
        },
        {
            name: _lt("Min"),
            types: [CellValueType.number],
            compute: (values) => MIN.compute([values]),
        },
        {
            name: _lt("Max"),
            types: [CellValueType.number],
            compute: (values) => MAX.compute([values]),
        },
        {
            name: _lt("Count"),
            types: [CellValueType.number, CellValueType.text, CellValueType.boolean, CellValueType.error],
            compute: (values) => COUNTA.compute([values]),
        },
        {
            name: _lt("Count Numbers"),
            types: [CellValueType.number, CellValueType.text, CellValueType.boolean, CellValueType.error],
            compute: (values) => COUNT.compute([values]),
        },
    ];
    /**
     * SelectionPlugin
     */
    class GridSelectionPlugin extends UIPlugin {
        constructor(getters, state, dispatch, config, selection) {
            super(getters, state, dispatch, config, selection);
            this.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 }],
            };
            this.selectedFigureId = null;
            this.sheetsData = {};
            // This flag is used to avoid to historize the ACTIVE_SHEET command when it's
            // the main command.
            this.activeSheet = null;
            this.moveClient = config.moveClient;
        }
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "ACTIVATE_SHEET":
                    try {
                        this.getters.getSheet(cmd.sheetIdTo);
                        break;
                    }
                    catch (error) {
                        return 27 /* CommandResult.InvalidSheetId */;
                    }
                case "MOVE_COLUMNS_ROWS":
                    return this.isMoveElementAllowed(cmd);
            }
            return 0 /* 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.dispatch("ACTIVATE_SHEET", {
                        sheetIdTo: firstSheetId,
                        sheetIdFrom: firstSheetId,
                    });
                    const { col, row } = this.getters.getNextVisibleCellPosition(firstSheetId, 0, 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": {
                    if (!this.getters.isSheetVisible(cmd.sheetIdTo)) {
                        this.dispatch("SHOW_SHEET", { sheetId: cmd.sheetIdTo });
                    }
                    this.setActiveSheet(cmd.sheetIdTo);
                    this.sheetsData[cmd.sheetIdFrom] = {
                        gridSelection: deepCopy(this.gridSelection),
                    };
                    if (cmd.sheetIdTo in this.sheetsData) {
                        Object.assign(this, this.sheetsData[cmd.sheetIdTo]);
                        this.selection.resetDefaultAnchor(this, deepCopy(this.gridSelection.anchor));
                    }
                    else {
                        const { col, row } = this.getters.getNextVisibleCellPosition(cmd.sheetIdTo, 0, 0);
                        this.selectCell(col, row);
                    }
                    const { col, row } = this.gridSelection.anchor.cell;
                    this.moveClient({ sheetId: this.activeSheet.id, col, row });
                    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),
                        };
                    }
                    this.fallbackToVisibleSheet();
                    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);
                    this.selectedFigureId = null;
                    break;
            }
        }
        finalize() {
            this.fallbackToVisibleSheet();
            /** 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;
        }
        getActiveCell() {
            const sheetId = this.getters.getActiveSheetId();
            const { col, row } = this.gridSelection.anchor.cell;
            const { col: mainCol, row: mainRow } = this.getters.getMainCellPosition(sheetId, col, row);
            return this.getters.getCell(sheetId, mainCol, mainRow);
        }
        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 cell = this.getters.getActiveCell();
            return cell ? this.getters.getCellStyle(cell) : {};
        }
        getSelectedZones() {
            return deepCopy(this.gridSelection.zones);
        }
        getSelectedZone() {
            return deepCopy(this.gridSelection.anchor.zone);
        }
        getSelection() {
            return deepCopy(this.gridSelection);
        }
        getSelectedFigureId() {
            return this.selectedFigureId;
        }
        getPosition() {
            return { col: this.gridSelection.anchor.cell.col, row: this.gridSelection.anchor.cell.row };
        }
        getSheetPosition(sheetId) {
            if (sheetId === this.getters.getActiveSheetId()) {
                return this.getPosition();
            }
            else {
                const sheetData = this.sheetsData[sheetId];
                return sheetData
                    ? {
                        col: sheetData.gridSelection.anchor.cell.col,
                        row: sheetData.gridSelection.anchor.cell.row,
                    }
                    : this.getters.getNextVisibleCellPosition(sheetId, 0, 0);
            }
        }
        getStatisticFnResults() {
            const sheetId = this.getters.getActiveSheetId();
            // get deduplicated cells in zones
            const cells = new Set(this.gridSelection.zones
                .map((zone) => this.getters.getCellsInZone(sheetId, zone))
                .flat()
                .filter((cell) => {
                if (!cell) {
                    return false;
                }
                const { col, row } = this.getters.getCellPosition(cell === null || cell === void 0 ? void 0 : cell.id);
                return !this.getters.isRowHidden(sheetId, row) && !this.getters.isColHidden(sheetId, col);
            }));
            let cellsTypes = new Set();
            let cellsValues = [];
            for (let cell of cells) {
                cellsTypes.add(cell.evaluated.type);
                cellsValues.push(cell.evaluated.value);
            }
            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);
                }
                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.getCell(sheetId, col, row);
                if ((cell === null || cell === void 0 ? void 0 : cell.evaluated.type) === CellValueType.number) {
                    n++;
                    aggregate += cell.evaluated.value;
                }
            }
            return n < 2 ? null : formatValue(aggregate);
        }
        isSelected(zone) {
            return !!this.getters.getSelectedZones().find((z) => isEqual(z, zone));
        }
        /**
         * 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
        // ---------------------------------------------------------------------------
        /**
         * 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,
            };
            this.setSelectionMixin(anchor, [selectedZone]);
        }
        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,
            };
            this.setSelectionMixin(anchor, [selectedZone]);
        }
        onAddElements(cmd) {
            const selection = this.gridSelection.anchor.zone;
            const zone = updateSelectionOnInsertion(selection, cmd.dimension === "COL" ? "left" : "top", cmd.base, cmd.position, cmd.quantity);
            const anchor = { cell: { col: zone.left, row: zone.top }, zone };
            this.setSelectionMixin(anchor, [zone]);
        }
        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 = isBasedBefore ? cmd.base : cmd.base + 1;
            for (const element of toRemove) {
                const size = cmd.dimension === "COL"
                    ? this.getters.getColSize(cmd.sheetId, element)
                    : this.getters.getRowSize(cmd.sheetId, 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 2 /* 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 87 /* CommandResult.InvalidHeaderIndex */;
            }
            return 0 /* CommandResult.Success */;
        }
        fallbackToVisibleSheet() {
            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,
                });
            }
        }
        //-------------------------------------------
        // 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;
            ctx.globalCompositeOperation = "multiply";
            for (const zone of zones) {
                const { x, y, width, height } = this.getters.getVisibleRect(zone);
                ctx.fillRect(x, y, width, height);
                ctx.strokeRect(x, y, width, height);
            }
            ctx.globalCompositeOperation = "source-over";
            // active zone
            const activeSheet = this.getters.getActiveSheetId();
            const { col, row } = this.getPosition();
            ctx.strokeStyle = SELECTION_BORDER_COLOR;
            ctx.lineWidth = 3 * thinLineWidth;
            let zone;
            if (this.getters.isInMerge(activeSheet, col, row)) {
                zone = this.getters.getMerge(activeSheet, col, row);
            }
            else {
                zone = {
                    top: row,
                    bottom: row,
                    left: col,
                    right: col,
                };
            }
            const { x, y, width, height } = this.getters.getVisibleRect(zone);
            if (width > 0 && height > 0) {
                ctx.strokeRect(x, y, width, height);
            }
        }
    }
    GridSelectionPlugin.layers = [5 /* LAYERS.Selection */];
    GridSelectionPlugin.getters = [
        "getActiveSheet",
        "getActiveSheetId",
        "getActiveCell",
        "getActiveCols",
        "getActiveRows",
        "getCurrentStyle",
        "getSelectedZones",
        "getSelectedZone",
        "getStatisticFnResults",
        "getAggregate",
        "getSelectedFigureId",
        "getSelection",
        "getPosition",
        "getSheetPosition",
        "isSelected",
        "getElementsFromSelection",
        "isGridSelectionActive",
    ];

    const uuidGenerator = new UuidGenerator();
    /**
     * 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 {
        constructor(getters, state, dispatch, config, selection, initialRanges, inputHasSingleRange) {
            if (inputHasSingleRange && initialRanges.length > 1) {
                throw new Error("Input with a single range cannot be instantiated with several range references.");
            }
            super(getters, state, dispatch, config, selection);
            this.inputHasSingleRange = inputHasSingleRange;
            this.ranges = [];
            this.focusedRangeIndex = null;
            this.willAddNewRange = false;
            this.insertNewRange(0, initialRanges);
            this.activeSheet = this.getters.getActiveSheetId();
            if (this.ranges.length === 0) {
                this.insertNewRange(this.ranges.length, [""]);
                this.focusLast();
            }
        }
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        handleEvent(event) {
            const xc = zoneToXc(event.anchor.zone);
            const inputSheetId = this.activeSheet;
            const sheetId = this.getters.getActiveSheetId();
            const sheetName = this.getters.getSheetName(sheetId);
            this.add([sheetId === inputSheetId ? xc : `${getComposerSheetName(sheetName)}!${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 values = cmd.value.split(",").map((reference) => reference.trim());
                        this.setRange(index, values);
                    }
                    break;
                }
                case "ADD_EMPTY_RANGE":
                    this.insertNewRange(this.ranges.length, [""]);
                    this.focusLast();
                    break;
                case "REMOVE_RANGE":
                    const index = this.getIndex(cmd.rangeId);
                    if (index !== null) {
                        this.removeRange(index);
                    }
                    break;
                case "STOP_SELECTION_INPUT":
                    this.willAddNewRange = false;
                    break;
                case "PREPARE_SELECTION_INPUT_EXPANSION": {
                    const index = this.focusedRangeIndex;
                    if (index !== null && !this.inputHasSingleRange) {
                        this.willAddNewRange = this.ranges[index].xc.trim() !== "";
                    }
                    break;
                }
                case "ACTIVATE_SHEET": {
                    if (cmd.sheetIdFrom !== cmd.sheetIdTo) {
                        const { col, row } = this.getters.getNextVisibleCellPosition(cmd.sheetIdTo, 0, 0);
                        const zone = this.getters.expandZone(cmd.sheetIdTo, positionToZone({ col, row }));
                        this.selection.resetAnchor(this, { cell: { col, row }, zone });
                    }
                }
            }
        }
        // ---------------------------------------------------------------------------
        // 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;
        }
        add(newRanges) {
            if (this.focusedRangeIndex === null || newRanges.length === 0) {
                return;
            }
            if (this.willAddNewRange) {
                this.insertNewRange(this.ranges.length, newRanges);
                this.focusLast();
                this.willAddNewRange = false;
            }
            else {
                this.setRange(this.focusedRangeIndex, newRanges);
            }
        }
        setContent(index, xc) {
            this.ranges[index] = {
                ...this.ranges[index],
                id: uuidGenerator.smallUuid(),
                xc,
            };
        }
        /**
         * Insert new inputs after the given index.
         */
        insertNewRange(index, values) {
            this.ranges.splice(index, 0, ...values.map((xc, i) => ({
                xc,
                id: (this.ranges.length + i + 1).toString(),
                color: getNextColor(),
            })));
        }
        /**
         * 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.activeSheet, reference));
            return XCs.map((xc) => {
                const { sheetName } = splitReference(xc);
                return {
                    zone: this.getters.getRangeFromSheetXC(this.activeSheet, xc).zone,
                    sheetId: (sheetName && this.getters.getSheetIdByName(sheetName)) || this.activeSheet,
                    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;
        }
    }
    SelectionInputPlugin.layers = [1 /* LAYERS.Highlights */];
    SelectionInputPlugin.getters = [];

    /**
     * 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 {
        get currentInput() {
            return this.focusedInputId ? this.inputs[this.focusedInputId] : null;
        }
        constructor(getters, state, dispatch, config, selection) {
            super(getters, state, dispatch, config, selection);
            this.state = state;
            this.config = config;
            this.inputs = {};
            this.focusedInputId = null;
        }
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        allowDispatch(cmd) {
            var _a, _b;
            switch (cmd.type) {
                case "FOCUS_RANGE":
                case "CHANGE_RANGE":
                case "ADD_EMPTY_RANGE":
                case "REMOVE_RANGE":
                    if (!this.inputs[cmd.id]) {
                        return 31 /* CommandResult.InvalidInputId */;
                    }
            }
            switch (cmd.type) {
                case "FOCUS_RANGE":
                    const index = (_a = this.currentInput) === null || _a === void 0 ? void 0 : _a.getIndex(cmd.rangeId);
                    if (this.focusedInputId === cmd.id && ((_b = this.currentInput) === null || _b === void 0 ? void 0 : _b.focusedRangeIndex) === index) {
                        return 29 /* CommandResult.InputAlreadyFocused */;
                    }
                    break;
                case "ADD_EMPTY_RANGE":
                    const input = this.inputs[cmd.id];
                    if (input.inputHasSingleRange && input.ranges.length === 1) {
                        return 30 /* CommandResult.MaximumRangesReached */;
                    }
                    break;
                case "CHANGE_RANGE": {
                    const input = this.inputs[cmd.id];
                    if (input.inputHasSingleRange && cmd.value.split(",").length > 1) {
                        return 30 /* CommandResult.MaximumRangesReached */;
                    }
                    break;
                }
            }
            return 0 /* CommandResult.Success */;
        }
        handle(cmd) {
            var _a;
            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_EMPTY_RANGE":
                case "REMOVE_RANGE":
                case "FOCUS_RANGE":
                case "CHANGE_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) });
                        this.focusedInputId = cmd.id;
                    }
                    break;
            }
            (_a = this.currentInput) === null || _a === void 0 ? void 0 : _a.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) {
            this.inputs[id] = new SelectionInputPlugin(this.getters, this.state, this.dispatch, this.config, this.selection, 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) });
                this.focusedInputId = id;
            }
        }
        unfocus() {
            this.selection.release(this.currentInput);
            this.focusedInputId = null;
        }
    }
    SelectionInputsManagerPlugin.layers = [1 /* LAYERS.Highlights */];
    SelectionInputsManagerPlugin.getters = [
        "getSelectionInput",
        "getSelectionInputValue",
        "isRangeValid",
        "getSelectionInputHighlights",
    ];

    /**
     * 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 {
        constructor() {
            this.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", ["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);
    function transformTargetSheetId(cmd, executed) {
        const deletedSheetId = executed.sheetId;
        if (cmd.targetSheetId === deletedSheetId || cmd.sheetId === deletedSheetId) {
            return undefined;
        }
        return cmd;
    }
    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(cmd, executed) {
        var _a;
        if (cmd.sheetId === executed.sheetId) {
            cmd = { ...cmd, sheetId: `${cmd.sheetId}~` };
        }
        if (cmd.name === executed.name) {
            return {
                ...cmd,
                name: ((_a = cmd.name) === null || _a === void 0 ? void 0 : _a.match(/\d+/))
                    ? cmd.name.replace(/\d+/, (n) => (parseInt(n) + 1).toString())
                    : `${cmd.name}~`,
                position: cmd.position + 1,
            };
        }
        return cmd;
    }
    function mergeTransformation(cmd, executed) {
        if (cmd.sheetId !== executed.sheetId) {
            return cmd;
        }
        const target = [];
        for (const zone1 of cmd.target) {
            if (executed.target.every((zone2) => !overlap(zone1, zone2))) {
                target.push(zone1);
            }
        }
        if (target.length) {
            return { ...cmd, target };
        }
        return undefined;
    }
    function freezeTransformation(cmd, executed) {
        if (cmd.sheetId !== executed.sheetId) {
            return cmd;
        }
        const dimension = cmd.type === "FREEZE_COLUMNS" ? "COL" : "ROW";
        if (dimension !== executed.dimension) {
            return cmd;
        }
        let quantity = cmd["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 ? { ...cmd, quantity } : undefined;
    }
    /**
     * Cancel CREATE_FILTER_TABLE and ADD_MERGE commands if they overlap a filter
     */
    function createTableTransformation(cmd, executed) {
        if (cmd.sheetId !== executed.sheetId) {
            return cmd;
        }
        for (const cmdTarget of cmd.target) {
            for (const executedCmdTarget of executed.target) {
                if (overlap(executedCmdTarget, cmdTarget)) {
                    return undefined;
                }
            }
        }
        return cmd;
    }

    const transformations = [
        { match: isSheetDependent, fn: transformSheetId },
        { match: isTargetDependent, fn: transformTarget },
        { match: isPositionDependent, fn: transformPosition },
        { match: isGridDependent, fn: transformDimension },
        { match: isRangeDependant, fn: transformRangeData },
    ];
    /**
     * 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];
        const possibleTransformations = new Set(otRegistry.getKeys());
        for (const executedCommand of executed) {
            // If the executed command is not in the registry, we skip it
            // because we know there won't be any transformation impacting the
            // commands to transform.
            if (possibleTransformations.has(executedCommand.type)) {
                transformedCommands = transformedCommands.reduce((acc, cmd) => {
                    const transformed = transform(cmd, executedCommand);
                    if (transformed) {
                        acc.push(transformed);
                    }
                    return acc;
                }, []);
            }
        }
        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(cmd, executed) {
        const deleteSheet = executed.type === "DELETE_SHEET" && executed.sheetId;
        if (cmd.sheetId === deleteSheet) {
            return "IGNORE_COMMAND";
        }
        else if (cmd.type === "CREATE_SHEET" ||
            executed.type === "CREATE_SHEET" ||
            cmd.sheetId !== executed.sheetId) {
            return cmd;
        }
        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 transformRangeData(cmd, executed) {
        const ranges = [];
        const deletedSheet = executed.type === "DELETE_SHEET" && executed.sheetId;
        for (const range of cmd.ranges) {
            if (range._sheetId !== executed.sheetId) {
                ranges.push({ ...range, _zone: range._zone });
            }
            else {
                const newZone = transformZone(range._zone, executed);
                if (newZone && deletedSheet !== range._sheetId) {
                    ranges.push({ ...range, _zone: newZone });
                }
            }
        }
        if (!ranges.length) {
            return "IGNORE_COMMAND";
        }
        return { ...cmd, ranges };
    }
    function transformDimension(cmd, executed) {
        const transformSheetResult = transformSheetId(cmd, executed);
        if (transformSheetResult !== "SKIP_TRANSFORMATION") {
            return transformSheetResult === "IGNORE_COMMAND" ? "IGNORE_COMMAND" : cmd;
        }
        if (executed.type === "ADD_COLUMNS_ROWS" || executed.type === "REMOVE_COLUMNS_ROWS") {
            if (executed.dimension !== cmd.dimension) {
                return cmd;
            }
            const isUnique = cmd.type === "ADD_COLUMNS_ROWS";
            const field = isUnique ? "base" : "elements";
            let elements = isUnique ? [cmd[field]] : cmd[field];
            if (executed.type === "REMOVE_COLUMNS_ROWS") {
                elements = elements
                    .map((element) => {
                    if (executed.elements.includes(element)) {
                        return undefined;
                    }
                    const executedElements = executed.elements.sort((a, b) => b - a);
                    for (let removedElement of executedElements) {
                        if (element > removedElement) {
                            element--;
                        }
                    }
                    return element;
                })
                    .filter(isDefined$1);
            }
            if (executed.type === "ADD_COLUMNS_ROWS") {
                const base = executed.position === "before" ? executed.base - 1 : executed.base;
                elements = elements.map((el) => (el > base ? el + executed.quantity : el));
            }
            if (elements.length) {
                let result = elements;
                if (isUnique) {
                    result = elements[0];
                }
                return { ...cmd, [field]: result };
            }
            return "IGNORE_COMMAND";
        }
        return "SKIP_TRANSFORMATION";
    }
    /**
     * Transform a PositionDependentCommand. It could be impacted by a grid command
     * (Add/remove cols/rows) and a merge
     */
    function transformPosition(cmd, executed) {
        const transformSheetResult = transformSheetId(cmd, executed);
        if (transformSheetResult !== "SKIP_TRANSFORMATION") {
            return transformSheetResult === "IGNORE_COMMAND" ? "IGNORE_COMMAND" : cmd;
        }
        if (executed.type === "ADD_COLUMNS_ROWS" || executed.type === "REMOVE_COLUMNS_ROWS") {
            return transformPositionWithGrid(cmd, executed);
        }
        if (executed.type === "ADD_MERGE") {
            return transformPositionWithMerge(cmd, executed);
        }
        return "SKIP_TRANSFORMATION";
    }
    /**
     * Transform a PositionDependentCommand after a grid shape modification. This
     * transformation consists of updating the position.
     */
    function transformPositionWithGrid(cmd, executed) {
        const field = executed.dimension === "COL" ? "col" : "row";
        let base = cmd[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 { ...cmd, [field]: base };
    }
    /**
     * Transform a PositionDependentCommand after a merge. This transformation
     * consists of checking that the position is not inside the merged zones
     */
    function transformPositionWithMerge(cmd, executed) {
        for (const zone of executed.target) {
            const sameTopLeft = cmd.col === zone.left && cmd.row === zone.top;
            if (!sameTopLeft && isInside(cmd.col, cmd.row, zone)) {
                return "IGNORE_COMMAND";
            }
        }
        return cmd;
    }

    class Revision {
        /**
         * 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, changes) {
            this._commands = [];
            this._changes = [];
            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 {
        /**
         * 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;
            /**
             * Positions of the others client.
             */
            this.clients = {};
            this.clientId = "local";
            this.pendingMessages = [];
            this.waitingAck = false;
            /**
             * Flag used to block all commands when an undo or redo is triggered, until
             * it is accepted on the server
             */
            this.waitingUndoRedoAck = false;
            this.isReplayingInitialRevisions = false;
            this.processedRevisions = new Set();
            this.uuidGenerator = new UuidGenerator();
            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(commands, changes) {
            if (!commands.length || !changes.length || !this.canApplyOptimisticUpdate())
                return;
            const revision = new Revision(this.uuidGenerator.uuidv4(), this.clientId, commands, changes);
            this.revisions.append(revision.id, 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;
        }
        _move(position) {
            var _a;
            // this method is debounced and might be called after the client
            // left the session.
            if (!this.clients[this.clientId])
                return;
            const currentPosition = (_a = this.clients[this.clientId]) === null || _a === void 0 ? void 0 : _a.position;
            if ((currentPosition === null || currentPosition === void 0 ? void 0 : 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 } = message;
                    const revision = new Revision(message.nextRevisionId, clientId, commands);
                    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", []);
                    this.revisions.insert(revision.id, revision, message.serverRevisionId);
                    this.dropPendingHistoryMessages();
                    this.trigger("snapshot");
                    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.sendPendingMessage();
        }
        /**
         * Send the next pending message
         */
        sendPendingMessage() {
            let message = this.pendingMessages[0];
            if (!message)
                return;
            if (message.type === "REMOTE_REVISION") {
                let revision = this.revisions.get(message.nextRevisionId);
                if (revision.commands.length === 0) {
                    /**
                     * The command is empty, we have to rebase all the next local revisions
                     * to avoid issues with undo/redo
                     */
                    this.revisions.rebase(revision.id);
                    revision = this.revisions.get(message.nextRevisionId);
                }
                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.waitingAck = true;
            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 "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;
                case "REVISION_UNDONE": {
                    this.waitingAck = false;
                    this.pendingMessages = this.pendingMessages.filter((msg) => msg.nextRevisionId !== message.nextRevisionId);
                    const firstPendingRevisionId = this.pendingMessages.findIndex((message) => message.type === "REMOTE_REVISION");
                    if (firstPendingRevisionId !== -1) {
                        /**
                         * Some revisions undergo transformations that may cause issues with
                         * undo/redo if the transformation is destructive (we don't get back
                         * the original command by transforming it with the inverse).
                         * To prevent these problems, we must rebase all subsequent local
                         * revisions.
                         */
                        this.revisions.rebase(this.pendingMessages[firstPendingRevisionId].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":
                case "SNAPSHOT_CREATED":
                    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 SelectionMultiUserPlugin extends UIPlugin {
        constructor() {
            super(...arguments);
            this.availableColors = new Set(colors);
            this.colors = {};
        }
        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;
        }
        /**
         * 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}`;
            }
        }
    }
    SelectionMultiUserPlugin.getters = ["getClientsToDisplay"];
    SelectionMultiUserPlugin.layers = [5 /* LAYERS.Selection */];

    class InternalViewport {
        constructor(getters, sheetId, boundaries, sizeInGrid, options, offsets) {
            this.getters = getters;
            this.sheetId = sheetId;
            this.boundaries = boundaries;
            if (sizeInGrid.width < 0 || sizeInGrid.height < 0) {
                throw new Error("Viewport size cannot be negative");
            }
            this.viewportWidth = sizeInGrid.height && sizeInGrid.width;
            this.viewportHeight = sizeInGrid.width && sizeInGrid.height;
            this.top = boundaries.top;
            this.bottom = boundaries.bottom;
            this.left = boundaries.left;
            this.right = boundaries.right;
            this.offsetX = offsets.x;
            this.offsetY = offsets.y;
            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
            }
            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, absolute = false) {
            if (x < this.offsetCorrectionX || x > this.offsetCorrectionX + this.viewportWidth) {
                return -1;
            }
            return this.searchHeaderIndex("COL", x - this.offsetCorrectionX, this.left, absolute);
        }
        /**
         * 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, absolute = false) {
            if (y < this.offsetCorrectionY || y > this.offsetCorrectionY + this.viewportHeight) {
                return -1;
            }
            return this.searchHeaderIndex("ROW", y - this.offsetCorrectionY, this.top, absolute);
        }
        /**
         * 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.col, position.row);
            const { col, row } = this.getters.getNextVisibleCellPosition(sheetId, mainCellPosition.col, mainCellPosition.row);
            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, true);
                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, true);
                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
         */
        getVisibleRect(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;
            }
        }
        getFullRect(zone) {
            const targetZone = intersection(zone, this.zone);
            if (targetZone) {
                const x = this.getters.getColRowOffset("COL", this.zone.left, zone.left) + this.offsetCorrectionX;
                const y = this.getters.getColRowOffset("ROW", this.zone.top, zone.top) + this.offsetCorrectionY;
                const width = this.getters.getColRowOffset("COL", zone.left, zone.right + 1);
                const height = this.getters.getColRowOffset("ROW", zone.top, zone.bottom + 1);
                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, absolute = false) {
            if (this.viewportWidth <= 0 || this.viewportHeight <= 0) {
                return -1;
            }
            let size = 0;
            const sheetId = this.sheetId;
            const headers = this.getters.getNumberHeaders(sheetId, dimension);
            for (let i = startIndex; i <= headers - 1; i++) {
                const isHiddenInViewport = !absolute && dimension === "COL"
                    ? i < this.left && i > this.right
                    : i < this.top && i > this.bottom;
                if (this.getters.isHeaderHidden(sheetId, dimension, i) || isHiddenInViewport) {
                    continue;
                }
                size +=
                    dimension === "COL"
                        ? this.getters.getColSize(sheetId, i)
                        : this.getters.getRowSize(sheetId, i);
                if (size > position) {
                    return i;
                }
            }
            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 one 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, true);
            this.right = this.getColIndex(this.offsetScrollbarX + this.viewportWidth, true);
            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 one 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, true);
            this.bottom = this.getRowIndex(this.offsetScrollbarY + this.viewportWidth, true);
            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.viewportWidth) {
                return;
            }
            if (this.left === -1) {
                this.left = this.boundaries.left;
            }
            if (this.right === -1) {
                this.right = this.boundaries.right;
            }
            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.viewportHeight) {
                return;
            }
            if (this.top === -1) {
                this.top = this.boundaries.top;
            }
            if (this.bottom === -1) {
                this.bottom = this.boundaries.bottom;
            }
            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 {
        constructor() {
            super(...arguments);
            this.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.
             */
            this.sheetViewWidth = DEFAULT_SHEETVIEW_SIZE;
            this.sheetViewHeight = DEFAULT_SHEETVIEW_SIZE;
            this.gridOffsetX = 0;
            this.gridOffsetY = 0;
            this.sheetsWithDirtyViewports = new Set();
        }
        // ---------------------------------------------------------------------------
        // 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 0 /* CommandResult.Success */;
            }
        }
        handleEvent(event) {
            switch (event.type) {
                case "HeadersSelected":
                case "AlterZoneCorner":
                    break;
                case "ZonesSelected":
                    const sheetId = this.getters.getActiveSheetId();
                    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.getMainInternalViewport(sheetId);
                        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;
                        }
                    }
                    col = Math.min(col, this.getters.getNumberCols(sheetId) - 1);
                    row = Math.min(row, this.getters.getNumberRows(sheetId) - 1);
                    this.refreshViewport(sheetId, { col, row });
                    break;
            }
        }
        handle(cmd) {
            var _a;
            switch (cmd.type) {
                case "START":
                    this.selection.observe(this, {
                        handleEvent: this.handleEvent.bind(this),
                    });
                    this.resetViewports(this.getters.getActiveSheetId());
                    break;
                case "UNDO":
                case "REDO":
                    this.resetSheetViews();
                    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 sheetId = this.getters.getActiveSheetId();
                    const { top, viewportHeight, offsetCorrectionY } = this.getMainInternalViewport(sheetId);
                    const topRowDims = this.getters.getRowDimensions(sheetId, top);
                    this.shiftVertically(topRowDims.start + viewportHeight - offsetCorrectionY);
                    break;
                case "SHIFT_VIEWPORT_UP": {
                    const sheetId = this.getters.getActiveSheetId();
                    const { top, viewportHeight, offsetCorrectionY } = this.getMainInternalViewport(sheetId);
                    const topRowDims = this.getters.getRowDimensions(sheetId, top);
                    this.shiftVertically(topRowDims.end - offsetCorrectionY - viewportHeight);
                    break;
                }
                case "REMOVE_COLUMNS_ROWS":
                case "RESIZE_COLUMNS_ROWS":
                case "HIDE_COLUMNS_ROWS":
                case "ADD_COLUMNS_ROWS":
                case "UNHIDE_COLUMNS_ROWS":
                case "UPDATE_FILTER":
                    this.resetViewports(cmd.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 || ((_a = cmd.style) === null || _a === void 0 ? void 0 : _a.fontSize) !== undefined) {
                        this.sheetsWithDirtyViewports.add(cmd.sheetId);
                    }
                    break;
                case "DELETE_SHEET":
                    this.cleanViewports();
                    this.sheetsWithDirtyViewports.delete(cmd.sheetId);
                    break;
                case "ACTIVATE_SHEET":
                    this.setViewports();
                    this.refreshViewport(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 "START_EDITION":
                    const { col, row } = this.getters.getPosition();
                    this.refreshViewport(this.getters.getActiveSheetId(), { col, row });
                    break;
            }
        }
        finalize() {
            for (const sheetId of this.sheetsWithDirtyViewports) {
                this.resetViewports(sheetId);
            }
            this.sheetsWithDirtyViewports = new Set();
            this.setViewports();
        }
        setViewports() {
            var _a;
            const sheetIds = this.getters.getSheetIds();
            for (const sheetId of sheetIds) {
                if (!((_a = this.viewports[sheetId]) === null || _a === void 0 ? void 0 : _a.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);
            return [...new Set(viewports.map((v) => range(v.left, v.right + 1)).flat())].filter((col) => col >= 0 && !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) => row >= 0 && !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, referenceHeaderIndex, targetHeaderIndex) {
            if (targetHeaderIndex < referenceHeaderIndex) {
                return -this.getColRowOffsetInViewport(dimension, targetHeaderIndex, referenceHeaderIndex);
            }
            const sheetId = this.getters.getActiveSheetId();
            const visibleHeaders = dimension === "COL"
                ? this.getters.getSheetViewVisibleCols()
                : this.getters.getSheetViewVisibleRows();
            const startIndex = visibleHeaders.findIndex((header) => referenceHeaderIndex >= header);
            let endIndex = visibleHeaders.findIndex((header) => targetHeaderIndex <= header);
            endIndex = endIndex === -1 ? visibleHeaders.length : endIndex;
            const relevantIndexes = visibleHeaders.slice(startIndex, endIndex);
            let offset = 0;
            for (const i of relevantIndexes) {
                offset +=
                    dimension === "COL"
                        ? this.getters.getColSize(sheetId, i)
                        : this.getters.getRowSize(sheetId, 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, startingY) {
            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 && startingY >= 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();
            return this.mapViewportsToRect(sheetId, (viewport) => viewport.getVisibleRect(zone));
        }
        /**
         * Computes the actual size and position (:Rect) of the zone on the canvas
         * regardless of the viewport dimensions.
         */
        getRect(zone) {
            const sheetId = this.getters.getActiveSheetId();
            return this.mapViewportsToRect(sheetId, (viewport) => viewport.getFullRect(zone));
        }
        /**
         * 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 };
        }
        getAllActiveViewportsZones() {
            const sheetId = this.getters.getActiveSheetId();
            return this.getSubViewports(sheetId);
        }
        // ---------------------------------------------------------------------------
        // 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 69 /* CommandResult.InvalidViewportSize */;
            }
            return 0 /* 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 77 /* CommandResult.ValuesNotChanged */;
            }
            return 0 /* CommandResult.Success */;
        }
        checkScrollingDirection({ offsetX, offsetY, }) {
            const pane = this.getMainInternalViewport(this.getters.getActiveSheetId());
            if ((!pane.canScrollHorizontally && offsetX > 0) ||
                (!pane.canScrollVertically && offsetY > 0)) {
                return 70 /* CommandResult.InvalidScrollingDirection */;
            }
            return 0 /* 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 newViewport = {};
            for (const sheetId of this.getters.getSheetIds()) {
                newViewport[sheetId] = this.viewports[sheetId];
            }
            this.viewports = newViewport;
        }
        resetSheetViews() {
            this.cleanViewports();
            for (let sheetId of Object.keys(this.viewports)) {
                const position = this.getters.getSheetPosition(sheetId);
                this.resetViewports(sheetId);
                const viewports = this.getSubViewports(sheetId);
                Object.values(viewports).forEach((viewport) => {
                    viewport.adjustPosition(position);
                });
            }
        }
        resizeSheetView(height, width, gridOffsetX = 0, gridOffsetY = 0) {
            this.sheetViewHeight = height;
            this.sheetViewWidth = width;
            this.gridOffsetX = gridOffsetX;
            this.gridOffsetY = gridOffsetY;
            this.recomputeViewports();
        }
        recomputeViewports() {
            for (const sheetId of this.getters.getSheetIds()) {
                this.resetViewports(sheetId);
            }
        }
        setSheetViewOffset(offsetX, offsetY) {
            const sheetId = this.getters.getActiveSheetId();
            const { maxOffsetX, maxOffsetY } = this.getMaximumSheetOffset();
            this.getSubViewports(sheetId).forEach((viewport) => viewport.setViewportOffset(clip(offsetX, 0, maxOffsetX), clip(offsetY, 0, maxOffsetY)));
        }
        getViewportOffset(sheetId) {
            var _a, _b;
            return {
                x: ((_a = this.viewports[sheetId]) === null || _a === void 0 ? void 0 : _a.bottomRight.offsetScrollbarX) || 0,
                y: ((_b = this.viewports[sheetId]) === null || _b === void 0 ? void 0 : _b.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 = Math.min(this.getters.getColRowOffset("COL", 0, xSplit, sheetId), this.sheetViewWidth);
            const rowOffset = Math.min(this.getters.getColRowOffset("ROW", 0, ySplit, sheetId), this.sheetViewHeight);
            const unfrozenWidth = Math.max(this.sheetViewWidth - colOffset, 0);
            const unfrozenHeight = Math.max(this.sheetViewHeight - rowOffset, 0);
            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: unfrozenWidth, 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: unfrozenHeight }, { 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: unfrozenWidth,
                    height: unfrozenHeight,
                }, { 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) {
            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 sheetId = this.getters.getActiveSheetId();
            const { top } = this.getMainInternalViewport(sheetId);
            const { scrollX } = this.getActiveSheetScrollInfo();
            this.setSheetViewOffset(scrollX, offset);
            const { anchor } = this.getters.getSelection();
            if (anchor.cell.row >= this.getters.getPaneDivisions(sheetId).ySplit) {
                const deltaRow = this.getMainInternalViewport(sheetId).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;
        }
        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 };
        }
        mapViewportsToRect(sheetId, rectCallBack) {
            let x = Infinity;
            let y = Infinity;
            let width = 0;
            let height = 0;
            let hasViewports = false;
            for (const viewport of this.getSubViewports(sheetId)) {
                const rect = rectCallBack(viewport);
                if (rect) {
                    hasViewports = true;
                    x = Math.min(x, rect.x);
                    y = Math.min(y, rect.y);
                    width = Math.max(width, rect.x + rect.width);
                    height = Math.max(height, rect.y + rect.height);
                }
            }
            if (!hasViewports) {
                return { x: 0, y: 0, width: 0, height: 0 };
            }
            return {
                x: x + this.gridOffsetX,
                y: y + this.gridOffsetY,
                width: width - x,
                height: height - y,
            };
        }
    }
    SheetViewPlugin.getters = [
        "getColIndex",
        "getRowIndex",
        "getActiveMainViewport",
        "getSheetViewDimension",
        "getSheetViewDimensionWithHeaders",
        "getMainViewportRect",
        "isVisibleInViewport",
        "getEdgeScrollCol",
        "getEdgeScrollRow",
        "getVisibleFigures",
        "getVisibleRect",
        "getColRowOffsetInViewport",
        "getMainViewportCoordinates",
        "getActiveSheetScrollInfo",
        "getActiveSheetDOMScrollInfo",
        "getSheetViewVisibleCols",
        "getSheetViewVisibleRows",
        "getFrozenSheetViewRatio",
        "getAllActiveViewportsZones",
        "getRect",
    ];

    class SortPlugin extends UIPlugin {
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "SORT_CELLS":
                    if (!isInside(cmd.col, cmd.row, cmd.zone)) {
                        throw new Error(_lt("The anchor must be part of the provided zone"));
                    }
                    return this.checkValidations(cmd, this.checkMerge, this.checkMergeSizes);
            }
            return 0 /* 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 0 /* CommandResult.Success */;
            }
            /*Test the presence of single cells*/
            for (let row = zone.top; row <= zone.bottom; row++) {
                for (let col = zone.left; col <= zone.right; col++) {
                    if (!this.getters.isInMerge(sheetId, col, row)) {
                        return 64 /* CommandResult.InvalidSortZone */;
                    }
                }
            }
            return 0 /* CommandResult.Success */;
        }
        checkMergeSizes({ sheetId, zone }) {
            if (!this.getters.doesIntersectMerge(sheetId, zone)) {
                return 0 /* 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.width, mergeDimension.height];
            if (!merges.every((merge) => {
                let [widthCurrent, heightCurrent] = [
                    merge.right - merge.left + 1,
                    merge.bottom - merge.top + 1,
                ];
                return widthCurrent === widthFirst && heightCurrent === heightFirst;
            })) {
                return 64 /* CommandResult.InvalidSortZone */;
            }
            return 0 /* 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: mainCellCol, row: mainCellRow } = this.getters.getMainCellPosition(sheetId, c, r);
                        cell = this.getters.getCell(sheetId, mainCellCol, mainCellRow);
                        if (cell === null || cell === void 0 ? void 0 : cell.formattedValue) {
                            return true;
                        }
                    }
                }
            }
            else {
                for (let cell of this.getters.getCellsInZone(sheetId, expandedZone)) {
                    if (cell === null || cell === void 0 ? void 0 : 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(items) {
            if (items[0].length === 1)
                return false;
            let cells = items.map((col) => col.map((cell) => (cell === null || cell === void 0 ? void 0 : cell.evaluated.type) || CellValueType.empty));
            // 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, anchor.col, anchor.row).col; // fetch anchor
            let sortZone = Object.assign({}, zone);
            // Update in case of merges in the zone
            let cells = this.mainCells(sheetId, zone);
            if (!options.sortHeaders && this.hasHeader(cells)) {
                sortZone.top += stepY;
            }
            cells = this.mainCells(sheetId, sortZone);
            const sortingCells = cells[sortingCol - sortZone.left];
            const sortedIndexOfSortTypeCells = sortCells(sortingCells, sortDirection, Boolean(options.emptyCellAsZero));
            const sortedIndex = sortedIndexOfSortTypeCells.map((x) => x.index);
            const [width, height] = [cells.length, cells[0].length];
            const cellUpdates = [];
            for (let c = 0; c < width; c++) {
                for (let r = 0; r < height; r++) {
                    let cell = cells[c][sortedIndex[r]];
                    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);
                            const offsetY = newRow - position.row;
                            // we only have a vertical offset
                            const ranges = this.getters.createAdaptedRanges(cell.dependencies, 0, offsetY, sheetId);
                            content = this.getters.buildFormulaContent(sheetId, cell, ranges);
                        }
                        newCellValues.style = cell.style;
                        newCellValues.content = content;
                        newCellValues.format = cell.format;
                    }
                    cellUpdates.push(newCellValues);
                }
            }
            cellUpdates.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, zone.left, 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(this.getters.getCell(sheetId, col, row));
                }
            }
            return cells;
        }
    }
    SortPlugin.getters = ["getContiguousZone"];

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

    class SheetUIPlugin extends UIPlugin {
        constructor() {
            super(...arguments);
            this.ctx = document.createElement("canvas").getContext("2d");
        }
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "AUTORESIZE_ROWS":
                case "AUTORESIZE_COLUMNS":
                    try {
                        this.getters.getSheet(cmd.sheetId);
                        break;
                    }
                    catch (error) {
                        return 27 /* CommandResult.InvalidSheetId */;
                    }
            }
            return 0 /* CommandResult.Success */;
        }
        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":
                    this.dispatch("RESIZE_COLUMNS_ROWS", {
                        elements: cmd.rows,
                        dimension: "ROW",
                        size: null,
                        sheetId: cmd.sheetId,
                    });
                    break;
            }
        }
        // ---------------------------------------------------------------------------
        // Getters
        // ---------------------------------------------------------------------------
        getCellWidth(sheetId, { col, row }) {
            const cell = this.getters.getCell(sheetId, col, row);
            let contentWidth = 0;
            if (cell) {
                contentWidth += this.getTextWidth(cell);
            }
            const icon = this.getters.getConditionalIcon(col, row);
            if (icon) {
                contentWidth += computeIconWidth(this.getters.getCellStyle(cell));
            }
            const isFilterHeader = this.getters.isFilterHeader(sheetId, col, row);
            if (isFilterHeader) {
                contentWidth += ICON_EDGE_LENGTH + FILTER_ICON_MARGIN;
            }
            if (contentWidth > 0) {
                contentWidth += 2 * PADDING_AUTORESIZE_HORIZONTAL;
                if (this.getters.getCellStyle(cell).wrapping === "wrap") {
                    const zone = positionToZone({ col, row });
                    const colWidth = this.getters.getColSize(this.getters.getActiveSheetId(), zone.left);
                    return Math.min(colWidth, contentWidth);
                }
            }
            return contentWidth;
        }
        getTextWidth(cell) {
            const text = this.getters.getCellText(cell, this.getters.shouldShowFormulas());
            const { sheetId, col, row } = this.getters.getCellPosition(cell.id);
            return computeTextWidth(this.ctx, text, this.getters.getCellComputedStyle(sheetId, col, row));
        }
        getCellText(cell, showFormula = false) {
            if (showFormula && (cell.isFormula() || cell.evaluated.type === CellValueType.error)) {
                return cell.content;
            }
            else {
                return cell.formattedValue;
            }
        }
        getCellMultiLineText(cell, width) {
            const style = this.getters.getCellStyle(cell);
            const text = this.getters.getCellText(cell);
            const words = text.split(" ");
            const brokenText = [];
            let textLine = "";
            let availableWidth = width;
            for (let word of words) {
                const splitWord = this.splitWordToSpecificWidth(this.ctx, word, width, style);
                const lastPart = splitWord.pop();
                const lastPartWidth = computeTextWidth(this.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(this.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;
        }
        /**
         * Returns the size, start and end coordinates of a column on an unfolded sheet
         */
        getColDimensions(sheetId, col) {
            const start = this.getColRowOffset("COL", 0, col, sheetId);
            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.getColRowOffset("ROW", 0, row, sheetId);
            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()) {
            if (index < referenceIndex) {
                return -this.getColRowOffset(dimension, index, referenceIndex);
            }
            let offset = 0;
            for (let i = referenceIndex; i < index; i++) {
                if (this.getters.isHeaderHidden(sheetId, dimension, i)) {
                    continue;
                }
                offset +=
                    dimension === "COL"
                        ? this.getters.getColSize(sheetId, i)
                        : this.getters.getRowSize(sheetId, i);
            }
            return offset;
        }
        // ---------------------------------------------------------------------------
        // 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, largeMax(sizes));
        }
        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;
        }
    }
    SheetUIPlugin.getters = [
        "getCellWidth",
        "getTextWidth",
        "getCellText",
        "getCellMultiLineText",
        "getColDimensions",
        "getRowDimensions",
        "getColRowOffset",
    ];

    const corePluginRegistry = new Registry()
        .add("sheet", SheetPlugin)
        .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);
    const uiPluginRegistry = new Registry()
        .add("selection", GridSelectionPlugin)
        .add("ui_sheet", SheetUIPlugin)
        .add("header_visibility_ui", HeaderVisibilityUIPlugin)
        .add("ui_options", UIOptionsPlugin)
        .add("evaluation", EvaluationPlugin)
        .add("evaluation_filter", FilterEvaluationPlugin)
        .add("evaluation_cf", EvaluationConditionalFormatPlugin)
        .add("evaluation_chart", EvaluationChartPlugin)
        .add("clipboard", ClipboardPlugin)
        .add("edition", EditionPlugin)
        .add("selectionInputManager", SelectionInputsManagerPlugin)
        .add("highlight", HighlightPlugin)
        .add("viewport", SheetViewPlugin)
        .add("grid renderer", RendererPlugin)
        .add("autofill", AutofillPlugin)
        .add("find_and_replace", FindAndReplacePlugin)
        .add("sort", SortPlugin)
        .add("automatic_sum", AutomaticSumPlugin)
        .add("format", FormatPlugin)
        .add("cell_popovers", CellPopoverPlugin)
        .add("selection_multiuser", SelectionMultiUserPlugin)
        .add("custom_colors", CustomColorsPlugin);

    const clickableCellRegistry = new Registry();
    clickableCellRegistry.add("link", {
        condition: (cell) => cell.isLink(),
        action: (cell, env) => {
            cell.action(env);
        },
        sequence: 5,
    });

    class FocusableElement {
        constructor() {
            this.focusableElement = undefined;
        }
        setFocusableElement(element) {
            this.focusableElement = element;
        }
        focus() {
            var _a;
            (_a = this.focusableElement) === null || _a === void 0 ? void 0 : _a.focus();
        }
    }

    // -----------------------------------------------------------------------------
    // SpreadSheet
    // -----------------------------------------------------------------------------
    css /* scss */ `
  .o-spreadsheet-bottom-bar {
    background-color: ${BACKGROUND_GRAY_COLOR};
    padding-left: ${HEADER_WIDTH}px;
    display: flex;
    align-items: center;
    font-size: 15px;
    border-top: 1px solid lightgrey;
    overflow: hidden;

    .o-add-sheet,
    .o-list-sheets {
      margin-right: 5px;
    }

    .o-add-sheet.disabled {
      cursor: not-allowed;
    }

    .o-sheet-item {
      display: flex;
      align-items: center;
      padding: 5px;
      cursor: pointer;
      &:hover {
        background-color: rgba(0, 0, 0, 0.08);
      }
    }

    .o-all-sheets {
      display: flex;
      align-items: center;
      max-width: 80%;
      overflow: hidden;
    }

    .o-sheet {
      color: #666;
      padding: 0 15px;
      padding-right: 10px;
      height: ${BOTTOMBAR_HEIGHT}px;
      line-height: ${BOTTOMBAR_HEIGHT}px;
      user-select: none;
      white-space: nowrap;
      border-left: 1px solid #c1c1c1;

      &:last-child {
        border-right: 1px solid #c1c1c1;
      }

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

      .o-sheet-icon {
        margin-left: 5px;

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

    .o-selection-statistic {
      background-color: #ffffff;
      margin-left: auto;
      font-size: 14px;
      margin-right: 20px;
      padding: 4px 8px;
      color: #333;
      border-radius: 3px;
      box-shadow: 0 1px 3px 1px rgba(60, 64, 67, 0.15);
      user-select: none;
      cursor: pointer;
      &:hover {
        background-color: rgba(0, 0, 0, 0.08);
      }
    }

    .fade-enter-active {
      transition: opacity 0.5s;
    }

    .fade-enter {
      opacity: 0;
    }
  }
`;
    class BottomBar extends owl.Component {
        constructor() {
            super(...arguments);
            this.bottomBarRef = owl.useRef("bottomBar");
            this.menuState = owl.useState({
                isOpen: false,
                menuId: undefined,
                position: null,
                menuItems: [],
            });
            this.selectedStatisticFn = "";
        }
        setup() {
            owl.onMounted(() => this.focusSheet());
            owl.onPatched(() => this.focusSheet());
        }
        focusSheet() {
            const div = this.bottomBarRef.el.querySelector(`[data-id="${this.env.model.getters.getActiveSheetId()}"]`);
            if (div && div.scrollIntoView) {
                div.scrollIntoView();
            }
        }
        addSheet() {
            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.smallUuid();
            const name = this.env.model.getters.getNextSheetName(this.env._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) => this.env.model.getters.getSheet(sheetId));
        }
        listSheets(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",
                    action: (env) => {
                        env.model.dispatch("ACTIVATE_SHEET", { sheetIdFrom: from, sheetIdTo: sheetId });
                    },
                });
                i++;
            }
            const target = ev.currentTarget;
            const { top, left } = target.getBoundingClientRect();
            this.openContextMenu(left, top, registry);
        }
        activateSheet(name) {
            this.env.model.dispatch("ACTIVATE_SHEET", {
                sheetIdFrom: this.env.model.getters.getActiveSheetId(),
                sheetIdTo: name,
            });
        }
        onDblClick(sheetId) {
            interactiveRenameSheet(this.env, sheetId);
        }
        openContextMenu(x, y, registry, menuId) {
            this.menuState.isOpen = true;
            this.menuState.menuItems = registry.getAll();
            this.menuState.position = { x, y };
            this.menuState.menuId = menuId;
        }
        onIconClick(sheetId, ev) {
            if (this.env.model.getters.getActiveSheetId() !== sheetId) {
                this.activateSheet(sheetId);
            }
            if (ev.closedMenuId === sheetId) {
                this.menuState.isOpen = false;
                this.menuState.menuId = undefined;
            }
            else {
                const target = ev.currentTarget.parentElement;
                const { top, left } = target.getBoundingClientRect();
                this.openContextMenu(left, top, sheetMenuRegistry, sheetId);
            }
        }
        onContextMenu(sheetId, ev) {
            if (this.env.model.getters.getActiveSheetId() !== sheetId) {
                this.activateSheet(sheetId);
            }
            const target = ev.currentTarget;
            const { top, left } = target.getBoundingClientRect();
            this.openContextMenu(left, top, sheetMenuRegistry, sheetId);
        }
        getSelectedStatistic() {
            const statisticFnResults = this.env.model.getters.getStatisticFnResults();
            // don't display button if no function has a result
            if (Object.values(statisticFnResults).every((result) => result === undefined)) {
                return undefined;
            }
            if (this.selectedStatisticFn === "") {
                this.selectedStatisticFn = Object.keys(statisticFnResults)[0];
            }
            return this.getComposedFnName(this.selectedStatisticFn, statisticFnResults[this.selectedStatisticFn]);
        }
        listSelectionStatistics(ev) {
            const registry = new MenuItemRegistry();
            let i = 0;
            for (let [fnName, fnValue] of Object.entries(this.env.model.getters.getStatisticFnResults())) {
                registry.add(fnName, {
                    name: this.getComposedFnName(fnName, fnValue),
                    sequence: i,
                    isReadonlyAllowed: true,
                    action: () => {
                        this.selectedStatisticFn = fnName;
                    },
                });
                i++;
            }
            const target = ev.currentTarget;
            const { top, left, width } = target.getBoundingClientRect();
            this.openContextMenu(left + width, top, registry);
        }
        getComposedFnName(fnName, fnValue) {
            return fnName + ": " + (fnValue !== undefined ? formatValue(fnValue) : "__");
        }
    }
    BottomBar.template = "o-spreadsheet-BottomBar";
    BottomBar.components = { Menu };

    css /* scss */ `
  .o-dashboard-clickable-cell {
    position: absolute;
    cursor: pointer;
  }
`;
    class SpreadsheetDashboard extends owl.Component {
        setup() {
            const gridRef = owl.useRef("grid");
            this.canvasPosition = useAbsolutePosition(gridRef);
            this.hoveredCell = owl.useState({ col: undefined, row: undefined });
            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 `
      max-width: ${end}px;
    `;
        }
        get gridOverlayDimensions() {
            return `
      height: 100%;
      width: 100%
    `;
        }
        getCellClickableStyle(coordinates) {
            return `
      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 cell = this.env.model.getters.getCell(sheetId, col, row);
                    if (cell) {
                        const action = this.getClickableAction(cell);
                        if (!action) {
                            continue;
                        }
                        let zone;
                        if (this.env.model.getters.isInMerge(sheetId, col, row)) {
                            zone = this.env.model.getters.getMerge(sheetId, col, row);
                        }
                        else {
                            zone = positionToZone({ col, row });
                        }
                        const rect = this.env.model.getters.getVisibleRect(zone);
                        cells.push({
                            coordinates: rect,
                            cell,
                            action,
                        });
                    }
                }
            }
            return cells;
        }
        getClickableAction(cell) {
            for (const items of clickableCellRegistry.getAll().sort((a, b) => a.sequence - b.sequence)) {
                if (items.condition(cell, this.env)) {
                    return items.action;
                }
            }
            return false;
        }
        selectClickableCell(clickableCell) {
            const { cell, action } = clickableCell;
            action(cell, 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,
            });
        }
    }
    SpreadsheetDashboard.template = "o-spreadsheet-SpreadsheetDashboard";
    SpreadsheetDashboard.components = {
        GridOverlay,
        GridPopover,
        Popover,
        VerticalScrollBar,
        HorizontalScrollBar,
        FilterIconsOverlay,
    };

    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-sidepanel-error {
      color: red;
      margin-top: 10px;
    }

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

    .o-sidePanelButton {
      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);
      }
    }
    .o-sidePanelButton:enabled {
      cursor: pointer;
    }

    .o-sidePanelButton:disabled {
      color: ${DISABLED_TEXT_COLOR};
    }

    .o-sidePanelButton: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;
      .o-type-selector {
        background-position: right 5px top 11px;
      }
    }
    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-inflection {
      .o-inflection-icon-button {
        display: inline-block;
        border: 1px solid #dadce0;
        border-radius: 4px;
        cursor: pointer;
        padding: 1px 2px;
      }
      .o-inflection-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-inflection-iconset-icons {
        width: 8%;
      }
      th.o-inflection-iconset-text {
        width: 28%;
      }
      th.o-inflection-iconset-operator {
        width: 14%;
      }
      th.o-inflection-iconset-type {
        width: 28%;
      }
      th.o-inflection-iconset-value {
        width: 26%;
      }
      input,
      select {
        width: 100%;
        height: 100%;
        box-sizing: border-box;
      }
    }

    .o-dropdown {
      position: relative;
      .o-dropdown-content {
        position: absolute;
        top: calc(100% + 5px);
        left: 0;
        z-index: ${ComponentsImportance.Dropdown};
        box-shadow: 1px 2px 5px 2px rgba(51, 51, 51, 0.15);
        background-color: #f6f6f6;

        .o-dropdown-item {
          padding: 7px 10px;
        }
        .o-dropdown-item:hover {
          background-color: rgba(0, 0, 0, 0.08);
        }
        .o-dropdown-line {
          display: flex;
          padding: 3px 6px;
          .o-line-item {
            width: 16px;
            height: 16px;
            margin: 1px 3px;
            &:hover {
              background-color: rgba(0, 0, 0, 0.08);
            }
          }
        }
      }
    }

    .o-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;
      }

      .o-tool.active,
      .o-tool:not(.o-disabled):hover {
        background-color: rgba(0, 0, 0, 0.08);
      }

      .o-with-color > span {
        border-bottom: 4px solid;
        height: 16px;
        margin-top: 2px;
      }
      .o-with-color {
        .o-line-item:hover {
          outline: 1px solid gray;
        }
      }
      .o-border {
        .o-line-item {
          padding: 4px;
          margin: 1px;
        }
      }
    }
  }
`;
    class SidePanel extends owl.Component {
        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.template = "o-spreadsheet-SidePanel";

    const AddMergeInteractiveContent = {
        MergeIsDestructive: _lt("Merging these cells will only preserve the top-leftmost value. Merge anyway?"),
        MergeInFilter: _lt("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(81 /* CommandResult.MergeInFilter */)) {
            env.raiseError(AddMergeInteractiveContent.MergeInFilter);
        }
        else if (result.isCancelledBecause(3 /* CommandResult.MergeIsDestructive */)) {
            env.askConfirmation(AddMergeInteractiveContent.MergeIsDestructive, () => {
                env.model.dispatch("ADD_MERGE", { sheetId, target, force: true });
            });
        }
    }

    const FORMATS = [
        { name: "automatic", text: NumberFormatTerms.Automatic },
        { name: "number", text: NumberFormatTerms.Number, description: "1,000.12", value: "#,##0.00" },
        { name: "percent", text: NumberFormatTerms.Percent, description: "10.12%", value: "0.00%" },
        {
            name: "currency",
            text: NumberFormatTerms.Currency,
            description: "$1,000.12",
            value: "[$$]#,##0.00",
        },
        {
            name: "currency_rounded",
            text: NumberFormatTerms.CurrencyRounded,
            description: "$1,000",
            value: "[$$]#,##0",
        },
        { name: "date", text: NumberFormatTerms.Date, description: "9/26/2008", value: "m/d/yyyy" },
        { name: "time", text: NumberFormatTerms.Time, description: "10:43:00 PM", value: "hh:mm:ss a" },
        {
            name: "datetime",
            text: NumberFormatTerms.DateTime,
            description: "9/26/2008 22:43:00",
            value: "m/d/yyyy hh:mm:ss",
        },
        {
            name: "duration",
            text: NumberFormatTerms.Duration,
            description: "27:51:38",
            value: "hhhh:mm:ss",
        },
    ];
    const CUSTOM_FORMATS = [
        { name: "custom_currency", text: NumberFormatTerms.CustomCurrency, sidePanel: "CustomCurrency" },
    ];
    // -----------------------------------------------------------------------------
    // TopBar
    // -----------------------------------------------------------------------------
    css /* scss */ `
  .o-spreadsheet-topbar {
    background-color: white;
    line-height: 1.2;
    display: flex;
    flex-direction: column;
    font-size: 13px;
    line-height: 1.2;
    user-select: none;

    .o-topbar-top {
      border-bottom: 1px solid #e0e2e4;
      display: flex;
      padding: 2px 10px;
      justify-content: space-between;

      /* Menus */
      .o-topbar-topleft {
        display: flex;
        .o-topbar-menu {
          padding: 4px 6px;
          margin: 0 2px;
          cursor: pointer;
        }

        .o-topbar-menu:hover,
        .o-topbar-menu-active {
          background-color: #f1f3f4;
          border-radius: 2px;
        }
      }

      .o-topbar-topright {
        display: flex;
        justify-content: flex-end;
      }
    }
    /* Toolbar + Cell Content */
    .o-topbar-toolbar {
      border-bottom: 1px solid #e0e2e4;
      display: flex;

      .o-readonly-toolbar {
        display: flex;
        align-items: center;
        background-color: ${BACKGROUND_HEADER_COLOR};
        padding-left: 18px;
        padding-right: 18px;
      }
      .o-composer-container {
        height: 34px;
        border: 1px solid #e0e2e4;
        margin-top: -1px;
        margin-bottom: -1px;
      }

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

        .o-tool {
          display: flex;
          align-items: center;
          margin: 2px;
          padding: 0 3px;
          border-radius: 2px;
          cursor: pointer;
          min-width: fit-content;
        }

        .o-tool-outlined {
          background-color: rgba(0, 0, 0, 0.08);
        }

        .o-filter-tool {
          margin-right: 8px;
        }

        .o-tool.active,
        .o-tool:not(.o-disabled):hover {
          background-color: #f1f3f4;
        }

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

        .o-with-color {
          .o-line-item:hover {
            outline: 1px solid gray;
          }
        }

        .o-border-dropdown {
          padding: 4px;
        }

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

        .o-disabled {
          opacity: 0.6;
          cursor: default;
        }

        .o-dropdown {
          position: relative;

          .o-text-icon {
            height: 100%;
            line-height: 30px;
          }

          .o-text-options > div {
            line-height: 26px;
            padding: 3px 12px;
            &:hover {
              background-color: rgba(0, 0, 0, 0.08);
            }
          }

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

            .o-dropdown-item {
              padding: 7px 10px;
            }

            .o-dropdown-item:hover {
              background-color: rgba(0, 0, 0, 0.08);
            }

            .o-dropdown-line {
              display: flex;
              margin: 1px;

              .o-line-item {
                padding: 4px;

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

            &.o-format-tool {
              padding: 5px 0;
              width: 250px;
              font-size: 12px;
              > div {
                padding: 0 20px;
                white-space: nowrap;

                &.active:before {
                  content: "✓";
                  font-weight: bold;
                  position: absolute;
                  left: 5px;
                }
              }
            }
          }
        }
      }

      /* Cell Content */
      .o-toolbar-cell-content {
        font-size: 12px;
        font-weight: 500;
        padding: 0 12px;
        margin: 0;
        line-height: 34px;
        white-space: nowrap;
        user-select: text;
      }
    }
  }
`;
    class TopBar extends owl.Component {
        constructor() {
            super(...arguments);
            this.DEFAULT_FONT_SIZE = DEFAULT_FONT_SIZE;
            this.commonFormats = FORMATS;
            this.customFormats = CUSTOM_FORMATS;
            this.currentFormatName = "automatic";
            this.fontSizes = fontSizes;
            this.style = {};
            this.state = owl.useState({
                menuState: { isOpen: false, position: null, menuItems: [] },
                activeTool: "",
            });
            this.isSelectingMenu = false;
            this.openedEl = null;
            this.inMerge = false;
            this.cannotMerge = false;
            this.undoTool = false;
            this.redoTool = false;
            this.paintFormatTool = false;
            this.fillColor = "#ffffff";
            this.textColor = "#000000";
            this.menus = [];
            this.composerStyle = `
    line-height: 34px;
    padding-left: 8px;
    height: 34px;
    background-color: white;
  `;
        }
        get dropdownStyle() {
            return `max-height:${this.props.dropdownMaxHeight}px`;
        }
        setup() {
            owl.useExternalListener(window, "click", this.onClick);
            owl.onWillStart(() => this.updateCellState());
            owl.onWillUpdateProps(() => this.updateCellState());
        }
        get topbarComponents() {
            return topbarComponentRegistry
                .getAll()
                .filter((item) => !item.isVisible || item.isVisible(this.env));
        }
        onClick(ev) {
            if (this.openedEl && isChildEvent(this.openedEl, ev)) {
                return;
            }
            this.closeMenus();
        }
        toogleStyle(style) {
            setStyle(this.env, { [style]: !this.style[style] });
        }
        toogleFormat(formatName) {
            const formatter = FORMATS.find((f) => f.name === formatName);
            const value = (formatter && formatter.value) || "";
            setFormatter(this.env, value);
        }
        toggleAlign(align) {
            setStyle(this.env, { ["align"]: align });
        }
        onMenuMouseOver(menu, ev) {
            if (this.isSelectingMenu) {
                this.toggleContextMenu(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) {
            this.closeMenus();
            const { left, top, height } = ev.target.getBoundingClientRect();
            this.state.menuState.isOpen = true;
            this.state.menuState.position = { x: left, y: top + height };
            this.state.menuState.menuItems = getMenuChildren(menu, this.env);
            this.state.menuState.parentMenu = menu;
            this.isSelectingMenu = true;
            this.openedEl = ev.target;
            this.env.model.dispatch("STOP_EDITION");
        }
        closeMenus() {
            this.state.activeTool = "";
            this.state.menuState.isOpen = false;
            this.state.menuState.parentMenu = undefined;
            this.isSelectingMenu = false;
            this.openedEl = null;
        }
        updateCellState() {
            const zones = this.env.model.getters.getSelectedZones();
            const sheetId = this.env.model.getters.getActiveSheetId();
            this.inMerge = false;
            const { top, left, right, bottom } = this.env.model.getters.getSelectedZone();
            const { xSplit, ySplit } = this.env.model.getters.getPaneDivisions(sheetId);
            this.cannotMerge =
                zones.length > 1 ||
                    (top === bottom && left === right) ||
                    (left < xSplit && xSplit <= right) ||
                    (top < ySplit && ySplit <= bottom);
            if (!this.cannotMerge) {
                const { col, row } = this.env.model.getters.getPosition();
                const zone = this.env.model.getters.expandZone(sheetId, {
                    left: col,
                    right: col,
                    top: row,
                    bottom: row,
                });
                this.inMerge = isEqual(zones[0], zone);
            }
            this.undoTool = this.env.model.getters.canUndo();
            this.redoTool = this.env.model.getters.canRedo();
            this.paintFormatTool = this.env.model.getters.isPaintingFormat();
            const cell = this.env.model.getters.getActiveCell();
            if (cell && cell.format) {
                const currentFormat = this.commonFormats.find((f) => f.value === cell.format);
                this.currentFormatName = currentFormat ? currentFormat.name : "";
            }
            else {
                this.currentFormatName = "automatic";
            }
            this.style = { ...this.env.model.getters.getCurrentStyle() };
            this.style.align = this.style.align || (cell === null || cell === void 0 ? void 0 : cell.defaultAlign);
            this.fillColor = this.style.fillColor || "#ffffff";
            this.textColor = this.style.textColor || "#000000";
            this.menus = topbarMenuRegistry.getAll();
        }
        getMenuName(menu) {
            return getMenuName(menu, this.env);
        }
        toggleMerge() {
            if (this.cannotMerge) {
                return;
            }
            const zones = this.env.model.getters.getSelectedZones();
            const target = [zones[zones.length - 1]];
            const sheetId = this.env.model.getters.getActiveSheetId();
            if (this.inMerge) {
                this.env.model.dispatch("REMOVE_MERGE", { sheetId, target });
            }
            else {
                interactiveAddMerge(this.env, sheetId, target);
            }
        }
        setColor(target, color) {
            setStyle(this.env, { [target]: color });
        }
        setBorder(command) {
            this.env.model.dispatch("SET_FORMATTING", {
                sheetId: this.env.model.getters.getActiveSheetId(),
                target: this.env.model.getters.getSelectedZones(),
                border: command,
            });
        }
        setFormat(ev) {
            const format = ev.target.dataset.format;
            if (format) {
                this.toogleFormat(format);
                return;
            }
            const custom = ev.target.dataset.custom;
            if (custom) {
                this.openCustomFormatSidePanel(custom);
            }
        }
        openCustomFormatSidePanel(custom) {
            const customFormatter = CUSTOM_FORMATS.find((c) => c.name === custom);
            const sidePanel = (customFormatter && customFormatter.sidePanel) || "";
            this.env.openSidePanel(sidePanel);
        }
        setDecimal(step) {
            this.env.model.dispatch("SET_DECIMAL", {
                sheetId: this.env.model.getters.getActiveSheetId(),
                target: this.env.model.getters.getSelectedZones(),
                step: step,
            });
        }
        paintFormat() {
            this.env.model.dispatch("ACTIVATE_PAINT_FORMAT", {
                target: this.env.model.getters.getSelectedZones(),
            });
        }
        clearFormatting() {
            this.env.model.dispatch("CLEAR_FORMATTING", {
                sheetId: this.env.model.getters.getActiveSheetId(),
                target: this.env.model.getters.getSelectedZones(),
            });
        }
        setSize(ev) {
            const fontSize = parseFloat(ev.target.dataset.size);
            setStyle(this.env, { fontSize });
        }
        doAction(action) {
            action(this.env);
            this.closeMenus();
        }
        undo() {
            this.env.model.dispatch("REQUEST_UNDO");
        }
        redo() {
            this.env.model.dispatch("REQUEST_REDO");
        }
        get selectionContainsFilter() {
            const sheetId = this.env.model.getters.getActiveSheetId();
            const selectedZones = this.env.model.getters.getSelectedZones();
            return this.env.model.getters.doesZonesContainFilter(sheetId, selectedZones);
        }
        get cannotCreateFilter() {
            return !areZonesContinuous(...this.env.model.getters.getSelectedZones());
        }
        createFilter() {
            if (this.cannotCreateFilter) {
                return;
            }
            const sheetId = this.env.model.getters.getActiveSheetId();
            const selection = this.env.model.getters.getSelectedZones();
            interactiveAddFilter(this.env, sheetId, selection);
        }
        removeFilter() {
            this.env.model.dispatch("REMOVE_FILTER_TABLE", {
                sheetId: this.env.model.getters.getActiveSheetId(),
                target: this.env.model.getters.getSelectedZones(),
            });
        }
    }
    TopBar.template = "o-spreadsheet-TopBar";
    TopBar.components = { ColorPicker, Menu, Composer };

    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;
    }

    &,
    *,
    *:before,
    *:after {
      box-sizing: content-box;
    }
    .o-separator {
      border-bottom: ${MENU_SEPARATOR_BORDER_WIDTH}px solid #e0e2e4;
      margin-top: ${MENU_SEPARATOR_PADDING}px;
      margin-bottom: ${MENU_SEPARATOR_PADDING}px;
    }
  }

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

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

  .o-cf-icon {
    width: ${CF_ICON_EDGE_LENGTH}px;
    height: ${CF_ICON_EDGE_LENGTH}px;
    vertical-align: sub;
  }
`;
    // -----------------------------------------------------------------------------
    // GRID STYLE
    // -----------------------------------------------------------------------------
    css /* scss */ `
  .o-grid {
    position: relative;
    overflow: hidden;
    background-color: ${BACKGROUND_GRAY_COLOR};
    &:focus {
      outline: none;
    }

    > canvas {
      border-top: 1px solid #e2e3e3;
      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;
    }
  }
`;
    const t = (s) => s;
    class Spreadsheet extends owl.Component {
        constructor() {
            super(...arguments);
            this.isViewportTooSmall = false;
        }
        getStyle() {
            if (this.env.isDashboard()) {
                return `grid-template-rows: auto;`;
            }
            return `grid-template-rows: ${TOPBAR_HEIGHT}px auto ${BOTTOMBAR_HEIGHT + 1}px`;
        }
        setup() {
            var _a, _b;
            (_b = (_a = this.props).exposeSpreadsheet) === null || _b === void 0 ? void 0 : _b.call(_a, this);
            this.model = this.props.model;
            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", {}),
            };
            owl.useSubEnv({
                model: this.model,
                isDashboard: () => this.model.getters.isDashboard(),
                openSidePanel: this.openSidePanel.bind(this),
                toggleSidePanel: this.toggleSidePanel.bind(this),
                _t: Spreadsheet._t,
                clipboard: navigator.clipboard,
                focusableElement: new FocusableElement(),
            });
            owl.useExternalListener(window, "resize", () => this.render(true));
            owl.useExternalListener(window, "beforeunload", this.unbindModelEvents.bind(this));
            // For some reason, the wheel event is not properly registered inside templates
            // in Chromium-based browsers based on chromium 125
            // This hack ensures the event declared in the template is properly registered/working
            owl.useExternalListener(document.body, "wheel", () => { });
            this.bindModelEvents();
            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, this.onNotifyUI);
        }
        unbindModelEvents() {
            this.model.off("update", this);
            this.model.off("notify-ui", this);
        }
        checkViewportSize() {
            const { xRatio, yRatio } = this.env.model.getters.getFrozenSheetViewRatio(this.env.model.getters.getActiveSheetId());
            if (yRatio > MAXIMAL_FREEZABLE_RATIO || xRatio > MAXIMAL_FREEZABLE_RATIO) {
                if (this.isViewportTooSmall) {
                    return;
                }
                this.env.notifyUser({
                    text: _lt("The current window is too small to display this sheet properly. Consider resizing your browser window or adjusting frozen rows and columns."),
                    tag: "viewportTooSmall",
                });
                this.isViewportTooSmall = true;
            }
            else {
                this.isViewportTooSmall = false;
            }
        }
        onNotifyUI(payload) {
            switch (payload.type) {
                case "ERROR":
                    this.env.raiseError(payload.text);
                    break;
            }
        }
        openSidePanel(panel, panelProps) {
            this.sidePanel.component = panel;
            this.sidePanel.panelProps = panelProps;
            this.sidePanel.isOpen = true;
        }
        closeSidePanel() {
            this.sidePanel.isOpen = false;
            this.focusGrid();
        }
        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();
        }
        save() {
            var _a, _b;
            (_b = (_a = this.props).onContentSaved) === null || _b === void 0 ? void 0 : _b.call(_a, this.model.exportData());
        }
        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.model.dispatch("UNFOCUS_SELECTION_INPUT");
            this.composer.topBarFocus = "contentFocus";
            this.composer.gridFocusMode = "inactive";
            this.setComposerContent({ selection });
        }
        onGridComposerContentFocused(selection) {
            if (this.model.getters.isReadonly()) {
                return;
            }
            this.model.dispatch("UNFOCUS_SELECTION_INPUT");
            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.model.dispatch("UNFOCUS_SELECTION_INPUT");
            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;
        }
    }
    Spreadsheet.template = "o-spreadsheet-Spreadsheet";
    Spreadsheet.components = { TopBar, Grid, BottomBar, SidePanel, SpreadsheetDashboard };
    Spreadsheet._t = t;

    class LocalTransportService {
        constructor() {
            this.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 {
        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() {
            var _a;
            return (_a = this.operations[this.operations.length - 1]) === null || _a === void 0 ? void 0 : _a.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 {
        constructor(id, data) {
            this.id = id;
            this.data = data;
        }
        transformed(transformation) {
            return new LazyOperation(this.id, lazy(() => transformation(this.data)));
        }
    }
    class LazyOperation {
        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 {
        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 {
        constructor(buildTransformation, initialBranch) {
            this.buildTransformation = buildTransformation;
            this.branchingOperationIds = new Map();
            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) {
            var _a;
            const insertAfter = branch.getLastOperationId() || ((_a = this.previousBranch(branch)) === null || _a === void 0 ? void 0 : _a.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
         * branches
         */
        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 {
        /**
         * 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(initialOperationId, applyOperation, revertOperation, buildEmpty, buildTransformation) {
            this.applyOperation = applyOperation;
            this.revertOperation = revertOperation;
            this.buildEmpty = buildEmpty;
            this.buildTransformation = buildTransformation;
            this.HEAD_BRANCH = new Branch(this.buildTransformation);
            this.tree = new Tree(buildTransformation, this.HEAD_BRANCH);
            const initial = new Operation(initialOperationId, 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);
        }
        rebase(operationId) {
            const operation = this.get(operationId);
            const execution = [...this.tree.execution(this.HEAD_BRANCH).startAfter(operationId)];
            this.revertBefore(operationId);
            const baseId = this.HEAD_OPERATION.id;
            this.tree.drop(operationId);
            this.insert(operationId, operation, baseId);
            for (const { operation } of execution) {
                this.insert(operation.id, operation.data, this.HEAD_OPERATION.id);
            }
        }
        /**
         * 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(initialRevisionId, recordChanges, dispatch) {
        return new SelectiveHistory(initialRevisionId, (revision) => {
            const commands = revision.commands.slice();
            const { changes } = recordChanges(() => {
                for (const command of commands) {
                    dispatch(command);
                }
            });
            revision.setChanges(changes);
        }, (revision) => revertChanges([revision]), (id) => new Revision(id, "empty", [], []), {
            with: (revision) => (toTransform) => {
                return new Revision(toTransform.id, toTransform.clientId, transformAll(toTransform.commands, revision.commands));
            },
            without: (revision) => (toTransform) => {
                return new Revision(toTransform.id, toTransform.clientId, transformAll(toTransform.commands, revision.commands.map(inverseCommand).flat()));
            },
        });
    }
    /**
     * 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[change.path.length - 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];
        }
    }

    /**
     * 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 LocalHistory extends owl.EventBus {
        constructor(dispatch, session) {
            super();
            this.dispatch = dispatch;
            this.session = session;
            /**
             * Ids of the revisions which can be undone
             */
            this.undoStack = [];
            /**
             * Ids of the revisions which can be redone
             */
            this.redoStack = [];
            this.session.on("new-local-state-update", this, this.onNewLocalStateUpdate);
            this.session.on("revision-undone", this, ({ commands }) => this.selectiveUndo(commands));
            this.session.on("revision-redone", this, ({ commands }) => this.selectiveRedo(commands));
            this.session.on("snapshot", this, () => {
                this.undoStack = [];
                this.redoStack = [];
            });
        }
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "REQUEST_UNDO":
                    if (!this.canUndo()) {
                        return 6 /* CommandResult.EmptyUndoStack */;
                    }
                    break;
                case "REQUEST_REDO":
                    if (!this.canRedo()) {
                        return 7 /* CommandResult.EmptyRedoStack */;
                    }
                    break;
            }
            return 0 /* CommandResult.Success */;
        }
        beforeHandle(cmd) { }
        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) {
                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() {
            return this.redoStack.length > 0;
        }
        onNewLocalStateUpdate({ id }) {
            this.undoStack.push(id);
            this.redoStack = [];
            if (this.undoStack.length > MAX_HISTORY_STEPS) {
                this.undoStack.shift();
            }
        }
        selectiveUndo(commands) {
            this.dispatch("UNDO", { commands });
        }
        selectiveRedo(commands) {
            this.dispatch("REDO", { commands });
        }
    }

    class RangeAdapter {
        constructor(getters) {
            this.providers = [];
            this.getters = getters;
        }
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        allowDispatch(cmd) {
            if (cmd.type === "MOVE_RANGES") {
                return cmd.target.length === 1 ? 0 /* CommandResult.Success */ : 26 /* CommandResult.InvalidZones */;
            }
            return 0 /* 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 = largeMin(group);
                            const max = largeMax(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 (isSheetNameEqual(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),
                };
                range = range.clone({ sheetId: copySheetId, zone: unboundZone });
                range.orderZone();
                return range;
            });
        }
        /**
         * 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) || !this.getters.tryGetSheet(defaultSheetId)) {
                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 };
            const range = new RangeImpl(rangeInterface, this.getters.getSheetSize);
            range.orderZone();
            return range;
        }
        /**
         * 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,
                },
            });
            return this.getRangeString(expandedRange, forSheetId);
        }
        /**
         * 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 [useFixedZone=false] if true, the range will be returned with fixed row and column
         */
        getRangeString(range, forSheetId, useFixedZone = false) {
            if (!range) {
                return INCORRECT_RANGE_STRING;
            }
            if (range.invalidXc) {
                return range.invalidXc;
            }
            if (!this.getters.tryGetSheet(range.sheetId)) {
                return INCORRECT_RANGE_STRING;
            }
            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 = getComposerSheetName(this.getters.getSheetName(rangeImpl.sheetId));
                }
            }
            if (prefixSheet && !sheetName) {
                return INCORRECT_RANGE_STRING;
            }
            let rangeString = this.getRangePartString(rangeImpl, 0, useFixedZone);
            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, useFixedZone);
                }
            }
            return `${prefixSheet ? sheetName + "!" : ""}${rangeString}`;
        }
        getRangeDataFromXc(sheetId, xc) {
            return this.getters.getRangeFromSheetXC(sheetId, xc).rangeData;
        }
        getRangeDataFromZone(sheetId, zone) {
            return { _sheetId: sheetId, _zone: zone };
        }
        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, useFixedZone = false) {
            var _a, _b;
            const colFixed = range.parts && ((_a = range.parts[part]) === null || _a === void 0 ? void 0 : _a.colFixed) ? "$" : "";
            const col = part === 0 ? numberToLetters(range.zone.left) : numberToLetters(range.zone.right);
            const rowFixed = range.parts && ((_b = range.parts[part]) === null || _b === void 0 ? void 0 : _b.rowFixed) ? "$" : "";
            const row = part === 0 ? String(range.zone.top + 1) : String(range.zone.bottom + 1);
            let str = "";
            if (range.isFullCol && !useFixedZone) {
                if (part === 0 && range.unboundedZone.hasHeader) {
                    str = colFixed + col + rowFixed + row;
                }
                else {
                    str = colFixed + col;
                }
            }
            else if (range.isFullRow && !useFixedZone) {
                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,
            };
        }
    }
    RangeAdapter.getters = [
        "getRangeString",
        "getSelectionRangeString",
        "getRangeFromSheetXC",
        "createAdaptedRanges",
        "getRangeDataFromXc",
        "getRangeDataFromZone",
        "getRangeFromRangeData",
    ];

    /**
     * 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 {
        constructor() {
            this.observers = [];
        }
        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.push({ owner, callbacks });
        }
        /**
         * Capture the stream for yourself
         */
        capture(owner, callbacks) {
            var _a, _b, _c;
            if (this.observers.find((sub) => sub.owner === owner)) {
                throw new Error("You are already subscribed forever");
            }
            if (((_a = this.mainSubscription) === null || _a === void 0 ? void 0 : _a.owner) && this.mainSubscription.owner !== owner) {
                (_c = (_b = this.mainSubscription.callbacks).release) === null || _c === void 0 ? void 0 : _c.call(_b);
            }
            this.mainSubscription = { owner, callbacks };
        }
        release(owner) {
            var _a;
            if (((_a = this.mainSubscription) === null || _a === void 0 ? void 0 : _a.owner) !== owner ||
                this.observers.find((sub) => sub.owner === owner)) {
                return;
            }
            this.mainSubscription = this.defaultSubscription;
        }
        /**
         * Release whichever subscription in charge and get back to the default subscription
         */
        getBackToDefault() {
            var _a, _b, _c;
            if (this.mainSubscription === this.defaultSubscription) {
                return;
            }
            (_c = (_a = this.mainSubscription) === null || _a === void 0 ? void 0 : (_b = _a.callbacks).release) === null || _c === void 0 ? void 0 : _c.call(_b);
            this.mainSubscription = this.defaultSubscription;
        }
        /**
         * Check if you are currently the main stream consumer
         */
        isListening(owner) {
            var _a;
            return ((_a = this.mainSubscription) === null || _a === void 0 ? void 0 : _a.owner) === owner;
        }
        /**
         * Push an event to the stream and broadcast it to consumers
         */
        send(event) {
            var _a;
            (_a = this.mainSubscription) === null || _a === void 0 ? void 0 : _a.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 SelectionStreamProcessor {
        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();
        }
        /**
         * Select a new anchor
         */
        selectZone(anchor) {
            const sheetId = this.getters.getActiveSheetId();
            anchor = {
                ...anchor,
                zone: this.getters.expandZone(sheetId, anchor.zone),
            };
            return this.processEvent({
                type: "ZonesSelected",
                anchor,
                mode: "overrideSelection",
            });
        }
        /**
         * Select a single cell as the new anchor.
         */
        selectCell(col, row) {
            const zone = positionToZone({ col, row });
            return this.selectZone({ zone, cell: { col, row } });
        }
        /**
         * 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(84 /* 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({
                type: "AlterZoneCorner",
                mode: "updateAnchor",
                anchor: anchor,
            });
        }
        /**
         * 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({
                type: "ZonesSelected",
                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(84 /* 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 { cell: refCell, zone: refZone } = this.getReferenceAnchor();
            const { col: refCol, row: refRow } = refCell;
            // 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 = refZone.right <= right - n ? expand({ top, left, bottom, right: newRight }) : null;
                }
                if (deltaCol > 0) {
                    const newLeft = this.getNextAvailableCol(deltaCol, left + (n - 1), refRow);
                    result = left + n <= refZone.left ? expand({ top, left: newLeft, bottom, right }) : null;
                }
                if (deltaRow < 0) {
                    const newBottom = this.getNextAvailableRow(deltaRow, refCol, bottom - (n - 1));
                    result =
                        refZone.bottom <= bottom - n ? expand({ top, left, bottom: newBottom, right }) : null;
                }
                if (deltaRow > 0) {
                    const newTop = this.getNextAvailableRow(deltaRow, refCol, top + (n - 1));
                    result = top + n <= refZone.top ? expand({ top: newTop, left, bottom, right }) : null;
                }
                result = result ? organizeZone(result) : result;
                if (result && !isEqual(result, anchor.zone)) {
                    return this.processEvent({
                        type: "ZonesSelected",
                        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({
                type: "ZonesSelected",
                anchor: newAnchor,
                mode: "updateAnchor",
            });
        }
        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({
                type: "HeadersSelected",
                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({
                type: "HeadersSelected",
                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;
            /** 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;
            };
            // The whole sheet is selected, select the anchor cell
            if (isEqual(this.anchor.zone, this.getters.getSheetZone(sheetId))) {
                return this.selectZone({ ...anchor, zone: positionToZone(anchor.cell) });
            }
            let hasExpanded = false;
            let hasExpandedOnce = false;
            let zone = anchor.zone;
            do {
                hasExpandedOnce = hasExpandedOnce || hasExpanded;
                hasExpanded = false;
                const newZone = expandZone(zone);
                if (!isEqual(zone, newZone)) {
                    hasExpanded = true;
                    zone = newZone;
                    continue;
                }
            } while (hasExpanded);
            return hasExpandedOnce ? this.selectZone({ ...anchor, zone }) : this.selectAll();
        }
        /**
         * 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({
                type: "HeadersSelected",
                mode: "overrideSelection",
                anchor: { zone, cell: this.anchor.cell },
            });
        }
        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 !== 0 /* 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 16 /* 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 17 /* CommandResult.SelectionOutOfBound */;
            }
            return 0 /* CommandResult.Success */;
        }
        checkAnchorZoneOrThrow(anchor) {
            const result = this.checkAnchorZone(anchor);
            if (result === 16 /* 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.
         */
        getReferenceAnchor() {
            const sheetId = this.getters.getActiveSheetId();
            const anchor = this.anchor;
            const { left, right, top, bottom } = anchor.zone;
            const { col: anchorCol, row: anchorRow } = anchor.cell;
            const col = this.getters.isColHidden(sheetId, anchorCol)
                ? this.getters.findVisibleHeader(sheetId, "COL", left, right) || anchorCol
                : anchorCol;
            const row = this.getters.isRowHidden(sheetId, anchorRow)
                ? this.getters.findVisibleHeader(sheetId, "ROW", top, bottom) || anchorRow
                : anchorRow;
            const zone = this.getters.expandZone(sheetId, {
                left: col,
                right: col,
                top: row,
                bottom: row,
            });
            return {
                cell: { col, row },
                zone,
            };
        }
        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) {
            const sheet = this.getters.getActiveSheet();
            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.isCellEmpty(currentPosition, sheet.id) && !this.isCellEmpty(nextCellPosition, sheet.id)
                ? "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.isCellEmpty(nextCellPosition, sheet.id);
                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;
        }
        /**
         * Check if a cell is empty or undefined in the model. If the cell is part of a merge,
         * check if the merge containing the cell is empty.
         */
        isCellEmpty({ col, row }, sheetId = this.getters.getActiveSheetId()) {
            const mainCellPosition = this.getters.getMainCellPosition(sheetId, col, row);
            const cell = this.getters.getCell(sheetId, mainCellPosition.col, mainCellPosition.row);
            return !cell || cell.isEmpty();
        }
        /** 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 };
        }
    }

    class StateObserver {
        constructor() {
            this.changes = [];
            this.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[args.length - 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 = largeMax(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;
        const functions = functionRegistry.content;
        const tokens = tokenize(formula);
        const attrs = [];
        let node = escapeXml ``;
        const isExported = tokens
            .filter((tk) => tk.type === "FUNCTION")
            .every((tk) => functions[tk.value.toUpperCase()].isExported);
        const type = getCellType(cell.value);
        const exportedValue = adaptFormulaValueToExcel(cell.value);
        if (isExported) {
            const XlsxFormula = adaptFormulaToExcel(formula);
            node = escapeXml /*xml*/ `
      <f>
        ${XlsxFormula}
      </f>
      ${escapeXml /*xml*/ `<v>${exportedValue}</v>`}
    `;
            attrs.push(["t", type]);
            return { attrs, node };
        }
        else {
            // If the cell contains a non-exported formula and that is evaluates to
            // nothing* ,we don't export it.
            // * non-falsy value are relevant and so are 0 and FALSE, which only leaves
            // the empty string.
            if (exportedValue === "")
                return undefined;
            attrs.push(["t", type]);
            node = escapeXml /*xml*/ `<v>${exportedValue}</v>`;
            return { attrs, node };
        }
    }
    function addContent(content, sharedStrings, forceString = false) {
        let value = content;
        const attrs = [];
        if (!forceString && ["TRUE", "FALSE"].includes(value.trim())) {
            value = value === "TRUE" ? "1" : "0";
            attrs.push(["t", "b"]);
        }
        else if (forceString || !isNumber(value)) {
            value = pushElement(content, sharedStrings);
            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;
        });
        ast = convertAstNodes(ast, "REFERENCE", (ast) => {
            return ast.value === CellErrorType.InvalidReference ? { ...ast, value: "#REF!" } : ast;
        });
        return ast ? astToFormula(ast) : formulaText;
    }
    function adaptFormulaValueToExcel(formulaValue) {
        return formulaValue === CellErrorType.InvalidReference ? "#REF!" : formulaValue;
    }
    /**
     * 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);
        if (internalDate) {
            let format = [];
            if (value.match(mdyDateRegexp) || value.match(ymdDateRegexp)) {
                format.push("yyyy-mm-dd");
            }
            if (value.match(timeRegexp)) {
                format.push("hh:mm:ss");
            }
            return {
                ...ast,
                value: formatValue(internalDate.value, format.join(" ")),
            };
        }
        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 } };
        }
        ruleAttributes.push(["dxfId", pushElement(dxf, dxfs)]);
        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)} />`);
            const iconSetAttrs = [["iconSet", getIconSet(rule.icons)]];
            if (isIconSetReversed(rule.icons)) {
                iconSetAttrs.push(["reverse", "1"]);
            }
            conditionalFormats.push(escapeXml /*xml*/ `
      <conditionalFormatting sqref="${range}">
        <cfRule ${formatAttributes(ruleAttributes)}>
          <iconSet ${formatAttributes(iconSetAttrs)}>
            ${joinXmlNodes(cfValueObjectNodes)}
          </iconSet>
        </cfRule>
      </conditionalFormatting>
    `);
        }
        return joinXmlNodes(conditionalFormats);
    }
    // ----------------------
    //         MISC
    // ----------------------
    function commonCfAttributes(cf) {
        return [
            ["priority", 1],
            ["stopIfTrue", cf.stopIfTrue ? 1 : 0],
        ];
    }
    function isIconSetReversed(iconSet) {
        const defaultIconSet = ICON_SETS[detectIconsType(iconSet)];
        return iconSet.upper === defaultIconSet.bad && iconSet.lower === defaultIconSet.good;
    }
    function getIconSet(iconSet) {
        return XLSX_ICONSET_MAP[detectIconsType(iconSet)];
    }
    /**
     * Partial detection based on "upper" point only.
     * We support any arbitrary icon in the set, while excel doesn't allow
     * mixing icons from different types.
     */
    function detectIconsType(iconSet) {
        const type = Object.keys(ICON_SETS).find((type) => Object.values(ICON_SETS[type]).includes(iconSet.upper)) || "dots";
        return type;
    }
    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(chartRelIds, 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)) {
            // position
            const { from, to } = convertFigureData(figure, sheet);
            const chartId = convertChartId(figure.id);
            const cNvPrAttrs = [
                ["id", chartId],
                ["name", `Chart ${chartId}`],
                ["title", "Chart"],
            ];
            figuresNodes.push(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="${chartRelIds[figureIndex]}" />
            </a:graphicData>
          </a:graphic>
        </xdr:graphicFrame>
        <xdr:clientData fLocksWithSheet="0"/>
      </xdr:twoCellAnchor>
    `);
        }
        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 Object.entries(headers)) {
            if (currentPosition <= position && position < currentPosition + header.size) {
                return {
                    index: parseInt(headerIndex),
                    offset: convertDotValueToEMU(position - currentPosition + FIGURE_BORDER_SIZE),
                };
            }
            else {
                currentPosition += header.size;
            }
        }
        return {
            index: headers.length - 1,
            offset: convertDotValueToEMU(position - currentPosition + FIGURE_BORDER_SIZE),
        };
    }

    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"])} />
        <right ${formatBorderAttribute(border["right"])} />
        <top ${formatBorderAttribute(border["top"])} />
        <bottom ${formatBorderAttribute(border["bottom"])} />
        <diagonal ${formatBorderAttribute(border["diagonal"])} />
      </border>
    `);
        }
        return escapeXml /*xml*/ `
    <borders count="${borders.length}">
      ${joinXmlNodes(borderNodes)}
    </borders>
  `;
    }
    function formatBorderAttribute(description) {
        if (!description) {
            return escapeXml ``;
        }
        return formatAttributes([
            ["style", description.style],
            ["color", 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]);
            }
            styleNodes.push(escapeXml /*xml*/ `
      <xf ${formatAttributes(attributes)}>
        ${alignAttrs ? escapeXml /*xml*/ `<alignment ${formatAttributes(alignAttrs)} />` : ""}
      </xf>
    `);
        }
        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_STYLE = escapeXml /*xml*/ `<tableStyleInfo name="TableStyleLight8" showFirstColumn="0" showLastColumn="0" showRowStripes="0" showColumnStripes="0"/>`;
    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) {
        var _a;
        const tableZone = toZone(table.range);
        const columns = [];
        for (const i of range(0, zoneToDimension(tableZone).width)) {
            const colHeaderXc = toXC(tableZone.left + i, tableZone.top);
            const colName = ((_a = sheetData.cells[colHeaderXc]) === null || _a === void 0 ? void 0 : _a.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) {
                        const res = addFormula(cell);
                        if (!res) {
                            continue;
                        }
                        ({ attrs: additionalAttrs, node: cellNode } = res);
                    }
                    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) {
        var _a;
        const sheet = data.sheets[sheetIndex];
        const cells = sheet.cells;
        const linkNodes = [];
        for (const xc in cells) {
            const content = (_a = cells[xc]) === null || _a === void 0 ? void 0 : _a.content;
            if (content && isMarkdownLink(content)) {
                const { label, url } = parseMarkdownLink(content);
                if (isMarkdownSheetLink(content)) {
                    const sheetId = parseSheetLink(url);
                    const sheet = data.sheets.find((sheet) => sheet.id === sheetId);
                    const location = sheet ? `${sheet.name}!A1` : INCORRECT_RANGE_STRING;
                    linkNodes.push(escapeXml /*xml*/ `
          <hyperlink display="${label}" location="${location}" ref="${xc}"/>
        `);
                }
                else {
                    const linkRelId = addRelsToFile(construct.relsFiles, `xl/worksheets/_rels/sheet${sheetIndex}.xml.rels`, {
                        target: url,
                        type: XLSX_RELATION_TYPE.hyperlink,
                        targetMode: "External",
                    });
                    linkNodes.push(escapeXml /*xml*/ `
          <hyperlink r:id="${linkRelId}" ref="${xc}"/>
        `);
                }
            }
        }
        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}
    `;
        }
        let sheetView = escapeXml /*xml*/ `
      <sheetViews>
        <sheetView showGridLines="${sheet.areGridLinesVisible ? 1 : 0}" workbookViewId="0">
          ${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(data);
        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 charts = sheet.charts;
            if (charts.length) {
                const chartRelIds = [];
                for (const chart of 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,
                    });
                    chartRelIds.push(chartRelId);
                    files.push(createXMLFile(createChart(chart, sheetIndex, data), `xl/charts/chart${xlsxChartId}.xml`, "chart"));
                }
                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(chartRelIds, sheet, charts), `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 = [];
        for (const file of files) {
            if (file.contentType) {
                overrideNodes.push(createOverride("/" + file.path, CONTENT_TYPES[file.contentType]));
            }
        }
        const xml = escapeXml /*xml*/ `
    <Types xmlns="${NAMESPACE["Types"]}">
      <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml" />
      <Default Extension="xml" ContentType="application/xml" />
      ${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 {
        constructor(data = {}, config = {}, stateUpdateMessages = [], uuidGenerator = new UuidGenerator(), verboseImport = true) {
            super();
            this.corePlugins = [];
            this.uiPlugins = [];
            /**
             * In a collaborative context, some commands can be replayed, we have to ensure
             * that these commands are not replayed on the UI plugins.
             */
            this.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
             */
            this.renderers = [];
            /**
             * Internal status of the model. Important for command handling coordination
             */
            this.status = 0 /* Status.Ready */;
            this.handlers = [];
            this.coreHandlers = [];
            /**
             * 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
             */
            this.dispatch = (type, payload) => {
                const command = { type, ...payload };
                let status = this.status;
                if (this.getters.isReadonly() && !canExecuteInReadonly(command)) {
                    return new DispatchResult(68 /* CommandResult.Readonly */);
                }
                if (!this.session.canApplyOptimisticUpdate()) {
                    return new DispatchResult(65 /* 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(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);
                        }
                        else {
                            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.
             */
            this.dispatchFromCorePlugin = (type, payload) => {
                const command = { 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;
            };
            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.config.moveClient = this.session.move.bind(this.session);
            this.history = new LocalHistory(this.dispatchFromCorePlugin, this.session);
            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.getSelectionRangeString = this.range.getSelectionRangeString.bind(this.range);
            this.getters = {
                isReadonly: () => this.config.mode === "readonly" || this.config.mode === "dashboard",
                isDashboard: () => this.config.mode === "dashboard",
                canUndo: this.history.canUndo.bind(this.history),
                canRedo: this.history.canRedo.bind(this.history),
                getClient: this.session.getClient.bind(this.session),
                getConnectedClients: this.session.getConnectedClients.bind(this.session),
                isFullySynchronized: this.session.isFullySynchronized.bind(this.session),
            };
            this.uuidGenerator.setIsFastStrategy(true);
            // Initiate stream processor
            this.selection = new SelectionStreamProcessor(this.getters);
            this.coreHandlers.push(this.range);
            this.handlers.push(this.range);
            // registering plugins
            for (let Plugin of corePluginRegistry.getAll()) {
                this.setupCorePlugin(Plugin, workbookData);
            }
            Object.assign(this.getters, this.coreGetters);
            for (let Plugin of uiPluginRegistry.getAll()) {
                this.setupUiPlugin(Plugin);
            }
            this.uuidGenerator.setIsFastStrategy(false);
            this.handlers.push(this.history);
            // 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"),
            });
            // move in "Loading" mode where we ignore redundant calls to finalize triggered by the
            // replay of stateUpdateMessages in the session
            this.isLoading = true;
            // This should be done after construction of LocalHistory due to order of
            // events
            this.setupSessionEvents();
            // Load the initial revisions
            this.session.loadInitialMessages(stateUpdateMessages);
            this.joinSession();
            if (config.snapshotRequested) {
                this.session.snapshot(this.exportData());
            }
            // 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);
            this.isLoading = false;
            // ensure propre recomputation of plugin states after the message replays
            this.finalize();
        }
        joinSession() {
            this.session.join(this.config.client);
        }
        leaveSession() {
            this.session.leave();
        }
        setupUiPlugin(Plugin) {
            const plugin = new Plugin(this.getters, this.state, this.dispatch, this.config, this.selection);
            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);
            }
            this.uiPlugins.push(plugin);
            this.handlers.push(plugin);
            const layers = Plugin.layers.map((l) => [plugin, l]);
            this.renderers.push(...layers);
            this.renderers.sort((p1, p2) => p1[1] - p2[1]);
        }
        /**
         * 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.coreGetters, this.state, this.range, this.dispatchFromCorePlugin, this.config, this.uuidGenerator);
            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.uiPlugins, command);
                this.status = previousStatus;
            }
            this.finalize();
        }
        setupSession(revisionId) {
            const session = new Session(buildRevisionLog(revisionId, this.state.recordChanges.bind(this.state), (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-redone", this, this.finalize);
            this.session.on("revision-undone", this, 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.smallUuid(),
                name: _lt("Anonymous").toString(),
            };
            const transportService = config.transportService || new LocalTransportService();
            return {
                ...config,
                mode: config.mode || "normal",
                evalContext: config.evalContext || {},
                transportService,
                client,
                moveClient: () => { },
                snapshotRequested: false,
                notifyUI: (payload) => this.trigger("notify-ui", payload),
            };
        }
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        /**
         * Check if the given command is allowed by all the plugins and the history.
         */
        checkDispatchAllowed(command) {
            const results = this.handlers.map((handler) => handler.allowDispatch(command));
            if (results.some((r) => r !== 0 /* CommandResult.Success */)) {
                return new DispatchResult(results.flat());
            }
            return DispatchResult.Success;
        }
        finalize() {
            if (this.isLoading) {
                return;
            }
            this.status = 3 /* Status.Finalizing */;
            for (const h of this.handlers) {
                h.finalize();
            }
            this.status = 0 /* Status.Ready */;
        }
        /**
         * Dispatch the given command to the given handlers.
         * It will call `beforeHandle` and `handle`
         */
        dispatchToHandlers(handlers, command) {
            command = deepCopy(command);
            for (const handler of handlers) {
                handler.beforeHandle(command);
            }
            for (const handler of handlers) {
                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("STOP_EDITION", { cancel: true });
            }
            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);
        }
    }

    /**
     * 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,
        uiPluginRegistry,
        corePluginRegistry,
        rowMenuRegistry,
        sidePanelRegistry,
        figureRegistry,
        sheetMenuRegistry,
        chartSidePanelComponentRegistry,
        chartComponentRegistry,
        chartRegistry,
        topbarMenuRegistry,
        topbarComponentRegistry,
        clickableCellRegistry,
        otRegistry,
        inverseCommandRegistry,
        cellRegistry,
        cellPopoverRegistry,
    };
    const cellTypes = {
        LinkCell,
    };
    const helpers = {
        args,
        toBoolean,
        toJsDate,
        toNumber,
        toString,
        toXC,
        toZone,
        toCartesian,
        numberToLetters,
        createFullMenuItem,
        UuidGenerator,
        formatValue,
        computeTextWidth,
        isMarkdownLink,
        parseMarkdownLink,
        markdownLink,
        createEmptyWorkbookData,
        createEmptySheet,
        createEmptyExcelSheet,
        getDefaultChartJsRuntime,
        chartFontColor,
        getMenuChildren,
        ChartColors,
        EvaluationError,
        CellErrorLevel,
        getFillingMode,
        rgbaToHex,
        colorToRGBA,
        positionToZone,
        deepEquals,
    };
    const components = {
        ChartPanel,
        ChartFigure,
        ChartJsComponent,
        Grid,
        GridOverlay,
        ScorecardChart,
        LineConfigPanel,
        LineBarPieDesignPanel,
        BarConfigPanel,
        LineBarPieConfigPanel,
        GaugeChartConfigPanel,
        GaugeChartDesignPanel,
        ScorecardChartConfigPanel,
        ScorecardChartDesignPanel,
    };
    function addFunction(functionName, functionDescription) {
        functionRegistry.add(functionName, functionDescription);
    }

    exports.AbstractChart = AbstractChart;
    exports.CorePlugin = CorePlugin;
    exports.DATETIME_FORMAT = DATETIME_FORMAT;
    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.cellTypes = cellTypes;
    exports.compile = compile;
    exports.components = components;
    exports.convertAstNodes = convertAstNodes;
    exports.coreTypes = coreTypes;
    exports.findCellInNewZone = findCellInNewZone;
    exports.functionCache = functionCache;
    exports.helpers = helpers;
    exports.invalidateEvaluationCommands = invalidateEvaluationCommands;
    exports.load = load;
    exports.parse = parse;
    exports.readonlyAllowedCommands = readonlyAllowedCommands;
    exports.registries = registries;
    exports.setTranslationMethod = setTranslationMethod;
    exports.tokenize = tokenize;

    Object.defineProperty(exports, '__esModule', { value: true });


    __info__.version = '16.0.75';
    __info__.date = '2025-07-11T11:47:47.509Z';
    __info__.hash = 'c6d8a41';


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