// @odoo-module ignore
// Transpiled AOT with https://github.com/rrahir/spreadsheet-tools
/**
 * This file is generated by o-spreadsheet build tools. Do not edit it.
 * @see https://github.com/odoo/o-spreadsheet
 * @version 19.0.24
 * @date 2026-03-06T07:17:55.361Z
 * @hash 7e8f4d894
 */
odoo.define('@spreadsheet/o_spreadsheet/o_spreadsheet', ['@odoo/owl'], function (require) {
'use strict';
let __exports = {};

const { useEnv, useSubEnv, onWillUnmount, useComponent, status, Component, useRef, onMounted, useEffect, App, blockDom, useState, onPatched, useExternalListener, onWillUpdateProps, onWillStart, onWillPatch, xml, useChildSubEnv, markRaw, toRaw } = require('@odoo/owl');

function createActions(menuItems) {
    return menuItems.map(createAction).sort((a, b) => a.sequence - b.sequence);
}
let nextItemId = 1;
function createAction(item) {
    const name = item.name;
    const children = item.children;
    const description = item.description;
    const icon = item.icon;
    const secondaryIcon = item.secondaryIcon;
    const itemId = item.id || nextItemId++;
    const isEnabled = item.isEnabled ? item.isEnabled : () => true;
    return {
        id: itemId.toString(),
        name: typeof name === "function" ? name : () => name,
        isVisible: item.isVisible ? item.isVisible : () => true,
        isEnabled: isEnabled,
        isActive: item.isActive,
        execute: item.execute
            ? (env, isMiddleClick) => {
                if (isEnabled(env)) {
                    return item.execute(env, isMiddleClick);
                }
                return undefined;
            }
            : undefined,
        children: children
            ? (env) => {
                return children
                    .map((child) => (typeof child === "function" ? child(env) : child))
                    .flat()
                    .map(createAction)
                    .sort((a, b) => a.sequence - b.sequence);
            }
            : () => [],
        isReadonlyAllowed: item.isReadonlyAllowed || false,
        separator: item.separator || false,
        icon: typeof icon === "function" ? icon : () => icon || "",
        iconColor: item.iconColor,
        secondaryIcon: typeof secondaryIcon === "function" ? secondaryIcon : () => secondaryIcon || "",
        description: typeof description === "function" ? description : () => description || "",
        textColor: item.textColor,
        sequence: item.sequence || 0,
        onStartHover: item.onStartHover,
        onStopHover: item.onStopHover,
    };
}
function getMenuItemsAndSeparators(env, actions) {
    const menuItemsAndSeparators = [];
    for (let i = 0; i < actions.length; i++) {
        const menuItem = actions[i];
        if (menuItem.isVisible(env)) {
            menuItemsAndSeparators.push(menuItem);
        }
        if (menuItem.separator &&
            i !== actions.length - 1 && // no separator at the end
            menuItemsAndSeparators[menuItemsAndSeparators.length - 1] !== "separator" // no double separator
        ) {
            menuItemsAndSeparators.push("separator");
        }
    }
    if (menuItemsAndSeparators[menuItemsAndSeparators.length - 1] === "separator") {
        menuItemsAndSeparators.pop();
    }
    if (menuItemsAndSeparators.length === 1 && menuItemsAndSeparators[0] === "separator") {
        return [];
    }
    return menuItemsAndSeparators;
}

/**
 * Registry
 *
 * The Registry class is basically just a mapping from a string key to an object.
 * It is really not much more than an object. It is however useful for the
 * following reasons:
 *
 * 1. it let us react and execute code when someone add something to the registry
 *   (for example, the FunctionRegistry subclass this for this purpose)
 * 2. it throws an error when the get operation fails
 * 3. it provides a chained API to add items to the registry.
 */
class Registry {
    content = {};
    /**
     * Add an item to the registry, you can only add if there is no item
     * already present in the registery with the given key
     *
     * Note that this also returns the registry, so another add method call can
     * be chained
     */
    add(key, value) {
        if (key in this.content) {
            throw new Error(`${key} is already present in this registry!`);
        }
        return this.replace(key, value);
    }
    /**
     * Replace (or add) an item to the registry
     *
     * Note that this also returns the registry, so another add method call can
     * be chained
     */
    replace(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];
    }
}

const CANVAS_SHIFT = 0.5;
// Colors
const HIGHLIGHT_COLOR = "#017E84";
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 = HIGHLIGHT_COLOR;
const FILTERS_COLOR = "#188038";
const SEPARATOR_COLOR = "#E0E2E4";
const ICONS_COLOR = "#4A4F59";
const HEADER_GROUPING_BACKGROUND_COLOR = "#F5F5F5";
const HEADER_GROUPING_BORDER_COLOR = "#999";
const GRID_BORDER_COLOR = "#E2E3E3";
const FROZEN_PANE_HEADER_BORDER_COLOR = "#BCBCBC";
const FROZEN_PANE_BORDER_COLOR = "#DADFE8";
const COMPOSER_ASSISTANT_COLOR = "#9B359B";
const COLOR_TRANSPARENT = "#00000000";
const TABLE_HOVER_BACKGROUND_COLOR = "#017E8414";
const CHART_WATERFALL_POSITIVE_COLOR = "#4EA7F2";
const CHART_WATERFALL_NEGATIVE_COLOR = "#EA6175";
const CHART_WATERFALL_SUBTOTAL_COLOR = "#AAAAAA";
const GRAY_900 = "#111827";
const GRAY_300 = "#D8DADD";
const GRAY_200 = "#E7E9ED";
const GRAY_100 = "#F9FAFB";
const TEXT_BODY = "#374151";
const TEXT_BODY_MUTED = TEXT_BODY + "C2";
const TEXT_HEADING = "#111827";
const PRIMARY_BUTTON_BG = "#714B67";
const PRIMARY_BUTTON_HOVER_BG = "#624159";
const PRIMARY_BUTTON_ACTIVE_BG = "#f1edf0";
const BUTTON_BG = GRAY_200;
const BUTTON_HOVER_BG = GRAY_300;
const BUTTON_HOVER_TEXT_COLOR = "#111827";
const BUTTON_ACTIVE_BG = "#e6f2f3";
const BUTTON_ACTIVE_TEXT_COLOR = "#111827";
const ACTION_COLOR = HIGHLIGHT_COLOR;
const ACTION_COLOR_HOVER = "#01585c";
const ALERT_WARNING_BG = "#FBEBCC";
const ALERT_WARNING_BORDER = "#F8E2B3";
const ALERT_WARNING_TEXT_COLOR = "#946D23";
const ALERT_DANGER_BG = "#D44C591A";
const ALERT_DANGER_BORDER = "#C34A41";
const ALERT_DANGER_TEXT_COLOR = "#C34A41";
const ALERT_INFO_BG = "#CDEDF1";
const ALERT_INFO_BORDER = "#98DBE2";
const ALERT_INFO_TEXT_COLOR = "#09414A";
const BADGE_SELECTED_COLOR = "#E6F2F3";
const CHART_PADDING = 20;
const CHART_PADDING_BOTTOM = 10;
const CHART_PADDING_TOP = 15;
const CHART_TITLE_FONT_SIZE = 16;
const CHART_AXIS_TITLE_FONT_SIZE = 12;
const MASTER_CHART_HEIGHT = 60;
const SCORECARD_CHART_TITLE_FONT_SIZE = 14;
const PIVOT_TOKEN_COLOR = "#F28C28";
// 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 DESKTOP_TOPBAR_TOOLBAR_HEIGHT = 34;
const MOBILE_TOPBAR_TOOLBAR_HEIGHT = 44;
const DESKTOP_BOTTOMBAR_HEIGHT = 36;
const MOBILE_BOTTOMBAR_HEIGHT = 44;
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 MIN_CF_ICON_MARGIN = 4;
const MIN_CELL_TEXT_MARGIN = 4;
const CF_ICON_EDGE_LENGTH = 15;
const PADDING_AUTORESIZE_VERTICAL = 3;
const PADDING_AUTORESIZE_HORIZONTAL = MIN_CELL_TEXT_MARGIN;
const GROUP_LAYER_WIDTH = 21;
const GRID_ICON_MARGIN = 2;
const GRID_ICON_EDGE_LENGTH = 17;
const FOOTER_HEIGHT = 2 * DEFAULT_CELL_HEIGHT;
const DATA_VALIDATION_CHIP_MARGIN = 5;
// 768px is a common breakpoint for small screens
// Typically inside Odoo, it is the threshold for switching to mobile view
const MOBILE_WIDTH_BREAKPOINT = 768;
// Menus
const MENU_WIDTH = 250;
const MENU_VERTICAL_PADDING = 6;
const DESKTOP_MENU_ITEM_HEIGHT = 26;
const MOBILE_MENU_ITEM_HEIGHT = 35;
const MENU_ITEM_PADDING_HORIZONTAL = 11;
const MENU_ITEM_PADDING_VERTICAL = 4;
const MENU_SEPARATOR_BORDER_WIDTH = 1;
const MENU_SEPARATOR_PADDING = 5;
// Style
const DEFAULT_STYLE = {
    align: "left",
    verticalAlign: "bottom",
    wrapping: "overflow",
    bold: false,
    italic: false,
    strikethrough: false,
    underline: false,
    fontSize: 10,
    fillColor: "",
    textColor: "",
};
const DEFAULT_NUMBER_STYLE = { ...DEFAULT_STYLE, align: "right" };
const DEFAULT_VERTICAL_ALIGN = DEFAULT_STYLE.verticalAlign;
const DEFAULT_WRAPPING_MODE = DEFAULT_STYLE.wrapping;
// Fonts
const DEFAULT_FONT_WEIGHT = "400";
const DEFAULT_FONT_SIZE = DEFAULT_STYLE.fontSize;
const HEADER_FONT_SIZE = 11;
const DEFAULT_FONT = "'Roboto', arial";
// Borders
const DEFAULT_BORDER_DESC = { style: "thin", color: "#000000" };
// Max Number of history steps kept in memory
const MAX_HISTORY_STEPS = 99;
// Id of the first revision
const DEFAULT_REVISION_ID = "START_REVISION";
// Figure
const DEFAULT_FIGURE_HEIGHT = 335;
const DEFAULT_FIGURE_WIDTH = 536;
const FIGURE_BORDER_WIDTH = 1;
const MIN_FIG_SIZE = 80;
// Chart
const MAX_CHAR_LABEL = 20;
const FIGURE_ID_SPLITTER = "??";
const DEFAULT_GAUGE_LOWER_COLOR = "#EA6175";
const DEFAULT_GAUGE_MIDDLE_COLOR = "#FFD86D";
const DEFAULT_GAUGE_UPPER_COLOR = "#43C5B1";
const DEFAULT_SCORECARD_BASELINE_MODE = "difference";
const DEFAULT_SCORECARD_BASELINE_COLOR_UP = "#43C5B1";
const DEFAULT_SCORECARD_BASELINE_COLOR_DOWN = "#EA6175";
const DEFAULT_SCORECARD_KEY_VALUE_FONT_SIZE = 32;
const DEFAULT_SCORECARD_BASELINE_FONT_SIZE = 16;
const LINE_FILL_TRANSPARENCY = 0.4;
const LINE_DATA_POINT_RADIUS = 3;
const DEFAULT_WINDOW_SIZE = 2;
// session
const DEBOUNCE_TIME = 200;
const MESSAGE_VERSION = 1;
// Sheets
const FORBIDDEN_SHEETNAME_CHARS = ["'", "*", "?", "/", "\\", "[", "]"];
const FORBIDDEN_SHEETNAME_CHARS_IN_EXCEL_REGEX = /'|\*|\?|\/|\\|\[|\]/;
// Cells
const FORMULA_REF_IDENTIFIER = "|";
// Components
var ComponentsImportance;
(function (ComponentsImportance) {
    ComponentsImportance[ComponentsImportance["Grid"] = 0] = "Grid";
    ComponentsImportance[ComponentsImportance["Highlight"] = 5] = "Highlight";
    ComponentsImportance[ComponentsImportance["HeaderGroupingButton"] = 6] = "HeaderGroupingButton";
    ComponentsImportance[ComponentsImportance["Figure"] = 10] = "Figure";
    ComponentsImportance[ComponentsImportance["ScrollBar"] = 15] = "ScrollBar";
    ComponentsImportance[ComponentsImportance["GridPopover"] = 19] = "GridPopover";
    ComponentsImportance[ComponentsImportance["GridComposer"] = 20] = "GridComposer";
    ComponentsImportance[ComponentsImportance["IconPicker"] = 25] = "IconPicker";
    ComponentsImportance[ComponentsImportance["TopBarComposer"] = 30] = "TopBarComposer";
    ComponentsImportance[ComponentsImportance["Popover"] = 35] = "Popover";
    ComponentsImportance[ComponentsImportance["FigureAnchor"] = 1000] = "FigureAnchor";
    ComponentsImportance[ComponentsImportance["FigureSnapLine"] = 1001] = "FigureSnapLine";
    ComponentsImportance[ComponentsImportance["FigureTooltip"] = 1002] = "FigureTooltip";
})(ComponentsImportance || (ComponentsImportance = {}));
let DEFAULT_SHEETVIEW_SIZE = 0;
function getDefaultSheetViewSize() {
    return DEFAULT_SHEETVIEW_SIZE;
}
function setDefaultSheetViewSize(size) {
    DEFAULT_SHEETVIEW_SIZE = size;
}
const MAXIMAL_FREEZABLE_RATIO = 0.85;
const NEWLINE = "\n";
const FONT_SIZES = [6, 7, 8, 9, 10, 11, 12, 14, 18, 24, 36];
// Pivot
const PIVOT_TABLE_CONFIG = {
    hasFilters: false,
    totalRow: false,
    firstColumn: true,
    lastColumn: false,
    numberOfHeaders: 1,
    bandedRows: true,
    bandedColumns: false,
    styleId: "TableStyleMedium5",
    automaticAutofill: false,
};
const PIVOT_INDENT = 15;
const PIVOT_COLLAPSE_ICON_SIZE = 12;
const PIVOT_MAX_NUMBER_OF_CELLS = 5e5;
const DEFAULT_CURRENCY = {
    symbol: "$",
    position: "before",
    decimalPlaces: 2,
    code: "",
    name: "Dollar",
};
const DEFAULT_CAROUSEL_TITLE_STYLE = {
    fontSize: CHART_TITLE_FONT_SIZE,
    color: TEXT_BODY,
};
const DEFAULT_TOKEN_COLOR = "#000000";
const functionColor = DEFAULT_TOKEN_COLOR;
const operatorColor = "#3da4ab";
const tokenColors = {
    OPERATOR: operatorColor,
    NUMBER: "#02c39a",
    STRING: "#00a82d",
    FUNCTION: functionColor,
    DEBUGGER: operatorColor,
    LEFT_PAREN: functionColor,
    RIGHT_PAREN: functionColor,
    ARG_SEPARATOR: functionColor,
    ORPHAN_RIGHT_PAREN: "#ff0000",
};
const DRAG_THRESHOLD = 5; // in pixels, to avoid unwanted drag when clicking

//------------------------------------------------------------------------------
// Miscellaneous
//------------------------------------------------------------------------------
const sanitizeSheetNameRegex = new RegExp(FORBIDDEN_SHEETNAME_CHARS_IN_EXCEL_REGEX, "g");
/**
 * 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) {
    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");
            }
            const result = Array.isArray(obj) ? new Array(obj.length) : {};
            if (Array.isArray(obj)) {
                for (let i = 0, len = obj.length; i < len; i++) {
                    if (i in obj) {
                        result[i] = deepCopy(obj[i]);
                    }
                }
            }
            else {
                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.constructor can be undefined when there's no prototype (`Object.create(null, {})`)
        (obj?.constructor === Object || obj?.constructor === undefined));
}
/**
 * Sanitize the name of a sheet, by eventually removing quotes
 * @param sheetName name of the sheet, potentially quoted with single quotes
 */
function getUnquotedSheetName(sheetName) {
    return unquote(sheetName, "'");
}
function unquote(string, quoteChar = '"') {
    if (string.startsWith(quoteChar)) {
        string = string.slice(1);
    }
    if (string.endsWith(quoteChar)) {
        string = string.slice(0, -1);
    }
    return string;
}
/**
 * Add quotes around the sheet name or any symbol name if it contains at least one non alphanumeric character
 * '\w' captures [0-9][a-z][A-Z] and _.
 * @param symbolName Name of the sheet or symbol
 */
function getCanonicalSymbolName(symbolName) {
    if (symbolName.match(/\w/g)?.length !== symbolName.length) {
        symbolName = `'${symbolName}'`;
    }
    return symbolName;
}
/** Replace the excel-excluded characters of a sheetName */
function sanitizeSheetName(sheetName, replacementChar = " ") {
    return sheetName.replace(sanitizeSheetNameRegex, replacementChar);
}
function clip(val, min, max) {
    return val < min ? min : val > max ? max : val;
}
/**
 * Create a range from start (included) to end (excluded).
 * range(10, 13) => [10, 11, 12]
 * range(2, 8, 2) => [2, 4, 6]
 */
function range(start, end, step = 1) {
    if (end <= start && step > 0) {
        return [];
    }
    if (step === 0) {
        throw new Error("range() step must not be zero");
    }
    const length = Math.ceil(Math.abs((end - start) / step));
    const array = Array(length);
    for (let i = 0; i < length; i++) {
        array[i] = start + i * step;
    }
    return array;
}
/**
 * Groups consecutive numbers.
 * The input array is assumed to be sorted
 * @param numbers
 */
function groupConsecutive(numbers) {
    return numbers.reduce((groups, currentRow, index, rows) => {
        if (Math.abs(currentRow - rows[index - 1]) === 1) {
            const lastGroup = groups[groups.length - 1];
            lastGroup.push(currentRow);
        }
        else {
            groups.push([currentRow]);
        }
        return groups;
    }, []);
}
/**
 * Create one generator from two generators by linking
 * each item of the first generator to the next item of
 * the second generator.
 *
 * Let's say generator G1 yields A, B, C and generator G2 yields X, Y, Z.
 * The resulting generator of `linkNext(G1, G2)` will yield A', B', C'
 * where `A' = A & {next: Y}`, `B' = B & {next: Z}` and `C' = C & {next: undefined}`
 * @param generator
 * @param nextGenerator
 */
function* linkNext(generator, nextGenerator) {
    nextGenerator.next();
    for (const item of generator) {
        const nextItem = nextGenerator.next();
        yield {
            ...item,
            next: nextItem.done ? undefined : nextItem.value,
        };
    }
}
function isBoolean(str) {
    const upperCased = str.toUpperCase();
    return upperCased === "TRUE" || upperCased === "FALSE";
}
const MARKDOWN_LINK_REGEX = /^\[(.+)\]\((.+)\)$/;
//link must start with http or https
//https://stackoverflow.com/a/3809435/4760614
const WEB_LINK_REGEX = /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/;
function isMarkdownLink(str) {
    return MARKDOWN_LINK_REGEX.test(str);
}
/**
 * Check if the string is a web link.
 * e.g. http://odoo.com
 */
function isWebLink(str) {
    return WEB_LINK_REGEX.test(str);
}
/**
 * Build a markdown link from a label and an url
 */
function markdownLink(label, url) {
    return `[${label}](${url})`;
}
function parseMarkdownLink(str) {
    const matches = str.match(MARKDOWN_LINK_REGEX) || [];
    const label = matches[1];
    const url = matches[2];
    if (!label || !url) {
        throw new Error(`Could not parse markdown link ${str}.`);
    }
    return {
        label,
        url,
    };
}
const O_SPREADSHEET_LINK_PREFIX = "o-spreadsheet://";
function isSheetUrl(url) {
    return url.startsWith(O_SPREADSHEET_LINK_PREFIX);
}
function buildSheetLink(sheetId) {
    return `${O_SPREADSHEET_LINK_PREFIX}${sheetId}`;
}
/**
 * Parse a sheet link and return the sheet id
 */
function parseSheetUrl(sheetLink) {
    if (sheetLink.startsWith(O_SPREADSHEET_LINK_PREFIX)) {
        return sheetLink.slice(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(argument) {
    return argument !== undefined;
}
/**
 * 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);
}
/**
 * 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.
 *
 * Also decorate the argument function with two methods: stopDebounce and isDebouncePending.
 *
 * Inspired by https://davidwalsh.name/javascript-debounce-function
 */
function debounce(func, wait, immediate) {
    let timeout = undefined;
    const debounced = function () {
        const context = this;
        const args = Array.from(arguments);
        function later() {
            timeout = undefined;
            if (!immediate) {
                func.apply(context, args);
            }
        }
        const callNow = immediate && !timeout;
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
        if (callNow) {
            func.apply(context, args);
        }
    };
    debounced.isDebouncePending = () => timeout !== undefined;
    debounced.stopDebounce = () => {
        clearTimeout(timeout);
    };
    return debounced;
}
/**
 * Creates a batched version of a callback so that all calls to it in the same
 * microtick will only call the original callback once.
 *
 * @param callback the callback to batch
 * @returns a batched version of the original callback
 *
 * Copied from odoo/owl repo.
 */
function batched(callback) {
    let scheduled = false;
    return async (...args) => {
        if (!scheduled) {
            scheduled = true;
            await Promise.resolve();
            scheduled = false;
            callback(...args);
        }
    };
}
/*
 * Concatenate an array of strings.
 */
function concat$1(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 n objects.
 */
function deepEquals(...o) {
    if (o.length <= 1)
        return true;
    for (let index = 1; index < o.length; index++) {
        if (!_deepEquals(o[0], o[index]))
            return false;
    }
    return true;
}
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
    for (const key in o2) {
        if (!(key in o1) && o2[key] !== undefined) {
            return false;
        }
    }
    for (const key in o1) {
        if (typeof o1[key] !== typeof o2[key])
            return false;
        if (typeof o1[key] === "object") {
            if (!_deepEquals(o1[key], o2[key]))
                return false;
        }
        else {
            if (o1[key] !== o2[key])
                return false;
        }
    }
    return true;
}
/**
 * Compares two arrays.
 * For performance reasons, this function is to be preferred
 * to 'deepEquals' in the case we know that the inputs are arrays.
 */
function deepEqualsArray(arr1, arr2) {
    if (arr1.length !== arr2.length) {
        return false;
    }
    for (let i = 0; i < arr1.length; i++) {
        if (!deepEquals(arr1[i], arr2[i])) {
            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) {
    if (!obj)
        return obj;
    const cleanObject = { ...obj };
    Object.keys(cleanObject).forEach((key) => !cleanObject[key] && delete cleanObject[key]);
    return cleanObject;
}
/**
 * Equivalent to "\s" in regexp, minus the new lines characters
 *
 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Character_Classes
 */
const specialWhiteSpaceSpecialCharacters = [
    "\t",
    "\f",
    "\v",
    String.fromCharCode(parseInt("00a0", 16)),
    String.fromCharCode(parseInt("1680", 16)),
    String.fromCharCode(parseInt("2000", 16)),
    String.fromCharCode(parseInt("200a", 16)),
    String.fromCharCode(parseInt("2028", 16)),
    String.fromCharCode(parseInt("2029", 16)),
    String.fromCharCode(parseInt("202f", 16)),
    String.fromCharCode(parseInt("205f", 16)),
    String.fromCharCode(parseInt("3000", 16)),
    String.fromCharCode(parseInt("feff", 16)),
];
const specialWhiteSpaceRegexp = new RegExp(specialWhiteSpaceSpecialCharacters.join("|"), "g");
const newLineRegexp = /(\r\n|\r)/g;
const whiteSpaceCharacters = specialWhiteSpaceSpecialCharacters.concat([" "]);
/**
 * Replace all different newlines characters by \n
 */
function replaceNewLines(text) {
    if (!text)
        return "";
    return text.replace(newLineRegexp, NEWLINE);
}
/**
 * Determine if the numbers are consecutive.
 */
function isConsecutive(iterable) {
    const array = Array.from(iterable).sort((a, b) => a - b); // sort numerically rather than lexicographically
    for (let i = 1; i < array.length; i++) {
        if (array[i] - array[i - 1] !== 1) {
            return false;
        }
    }
    return true;
}
/**
 * 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];
}
/**
 * Removes the specified indexes from the array.
 * Sparse (empty) elements are transformed to undefined (unless their index is explicitly removed).
 */
function removeIndexesFromArray(array, indexes) {
    const toRemove = new Set(indexes);
    const newArray = [];
    for (let i = 0; i < array.length; i++) {
        if (!toRemove.has(i)) {
            newArray.push(array[i]);
        }
    }
    return newArray;
}
function insertItemsAtIndex(array, items, index) {
    return array.slice(0, index).concat(items).concat(array.slice(index));
}
function replaceItemAtIndex(array, newItem, index) {
    const newArray = [...array];
    newArray[index] = newItem;
    return newArray;
}
function trimContent(content) {
    const contentLines = content.split("\n");
    return contentLines.map((line) => line.replace(/\s+/g, " ").trim()).join("\n");
}
function isNumberBetween(value, min, max) {
    if (min > max) {
        return isNumberBetween(value, max, min);
    }
    return value >= min && value <= max;
}
/**
 * Get a Regex for the find & replace that matches the given search string and options.
 */
function getSearchRegex(searchStr, searchOptions) {
    let searchValue = escapeRegExp(searchStr);
    const flags = !searchOptions.matchCase ? "i" : "";
    if (searchOptions.exactMatch) {
        searchValue = `^${searchValue}$`;
    }
    return RegExp(searchValue, flags);
}
/**
 * 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 < 100_000)
        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 < 100_000)
        return Math.min(...array);
    let min = +Infinity;
    while (len--) {
        min = array[len] < min ? array[len] : min;
    }
    return min;
}
class TokenizingChars {
    text;
    currentIndex = 0;
    current;
    constructor(text) {
        this.text = text;
        this.current = text[0];
    }
    shift() {
        const current = this.current;
        const next = this.text[++this.currentIndex];
        this.current = next;
        return current;
    }
    advanceBy(length) {
        this.currentIndex += length;
        this.current = this.text[this.currentIndex];
    }
    isOver() {
        return this.currentIndex >= this.text.length;
    }
    remaining() {
        return this.text.substring(this.currentIndex);
    }
    currentStartsWith(str) {
        if (this.current !== str[0]) {
            return false;
        }
        for (let j = 1; j < str.length; j++) {
            if (this.text[this.currentIndex + j] !== str[j]) {
                return false;
            }
        }
        return true;
    }
}
/**
 * Remove duplicates from an array.
 *
 * @param array The array to remove duplicates from.
 * @param cb A callback to get an element value.
 */
function removeDuplicates$1(array, cb = (a) => a) {
    const set = new Set();
    return array.filter((item) => {
        const key = cb(item);
        if (set.has(key)) {
            return false;
        }
        set.add(key);
        return true;
    });
}
/**
 * Similar to transposing and array, but with POJOs instead of arrays. Useful, for example, when manipulating
 * a POJO grid[col][row] and you want to transpose it to grid[row][col].
 *
 * The resulting object is created such as result[key1][key2] = pojo[key2][key1]
 */
function transpose2dPOJO(pojo) {
    const result = {};
    for (const key in pojo) {
        for (const subKey in pojo[key]) {
            if (!result[subKey]) {
                result[subKey] = {};
            }
            result[subKey][key] = pojo[key][subKey];
        }
    }
    return result;
}
function getUniqueText(text, texts, options = {}) {
    const compute = options.compute ?? ((text, i) => `${text} (${i})`);
    const computeFirstOne = options.computeFirstOne ?? false;
    let i = options.start ?? 1;
    let newText = computeFirstOne ? compute(text, i) : text;
    while (texts.includes(newText)) {
        newText = compute(text, i++);
    }
    return newText;
}
function isFormula(content) {
    return content.startsWith("=") || content.startsWith("+");
}
// TODO: we should make make ChartStyle be the same as Style sometime ...
function chartStyleToCellStyle(style) {
    return {
        bold: style.bold,
        italic: style.italic,
        fontSize: style.fontSize,
        textColor: style.color,
        align: style.align,
    };
}

const RBA_REGEX = /rgba?\(|\s+|\)/gi;
const HEX_MATCH = /^#([A-F\d]{2}){3,4}$/;
const colors = [
    "#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 hex (+alpha) value
 * */
function colorNumberToHex(color, alpha = 1) {
    const alphaHex = alpha !== 1
        ? Math.round(alpha * 255)
            .toString(16)
            .padStart(2, "0")
        : "";
    return toHex(color.toString(16).padStart(6, "0")) + alphaHex;
}
function colorToNumber(color) {
    if (typeof color === "number") {
        return color;
    }
    return Number.parseInt(toHex(color).slice(1, 7), 16);
}
/**
 * Converts any CSS color value to a standardized hex6 value.
 * Accepts: hex3, hex6, hex8, rgb[1] and rgba[1].
 *
 * [1] under the form rgb(r, g, b, a?) or rgba(r, g, b, a?)
 * with r,g,b ∈ [0, 255] and a ∈ [0, 1]
 *
 * toHex("#ABC")
 * >> "#AABBCC"
 *
 * toHex("#AAAFFF")
 * >> "#AAAFFF"
 *
 * toHex("rgb(30, 80, 16)")
 * >> "#1E5010"
 *
 *  * toHex("rgb(30, 80, 16, 0.5)")
 * >> "#1E501080"
 *
 */
function toHex(color) {
    let hexColor = color;
    if (color.startsWith("rgb")) {
        hexColor = rgbaStringToHex(color);
    }
    else {
        hexColor = color.replace("#", "").toUpperCase();
        if (hexColor.length === 3 || hexColor.length === 4) {
            hexColor = hexColor.split("").reduce((acc, h) => acc + h + h, "");
        }
        hexColor = `#${hexColor}`;
    }
    if (!HEX_MATCH.test(hexColor)) {
        throw new Error(`invalid color input: ${color}`);
    }
    return hexColor;
}
function isColorValid(color) {
    try {
        toHex(color);
        return true;
    }
    catch (error) {
        return false;
    }
}
function isHSLAValid(color) {
    try {
        hslaToHex(color);
        return true;
    }
    catch (error) {
        return false;
    }
}
const isColorValueValid = (v) => v >= 0 && v <= 255;
function rgba(r, g, b, a = 1) {
    const isInvalid = !isColorValueValid(r) || !isColorValueValid(g) || !isColorValueValid(b) || a < 0 || a > 1;
    if (isInvalid) {
        throw new Error(`Invalid RGBA values ${[r, g, b, a]}`);
    }
    return { a, b, g, r };
}
/**
 * The relative brightness of a point in the colorspace, normalized to 0 for
 * darkest black and 1 for lightest white.
 * https://www.w3.org/TR/WCAG20/#relativeluminancedef
 */
function relativeLuminance(color) {
    let { r, g, b } = colorToRGBA(color);
    r /= 255;
    g /= 255;
    b /= 255;
    const toLinearValue = (c) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
    const R = toLinearValue(r);
    const G = toLinearValue(g);
    const B = toLinearValue(b);
    return 0.2126 * R + 0.7152 * G + 0.0722 * B;
}
/**
 * Convert a CSS rgb color string to a standardized hex6 color value.
 *
 * rgbaStringToHex("rgb(30, 80, 16)")
 * >> "#1E5010"
 *
 * rgbaStringToHex("rgba(30, 80, 16, 0.5)")
 * >> "#1E501080"
 *
 * DOES NOT SUPPORT NON INTEGER RGB VALUES
 */
function rgbaStringToHex(color) {
    const stringVals = color.replace(RBA_REGEX, "").split(",");
    let alphaHex = 255;
    if (stringVals.length !== 3 && stringVals.length !== 4) {
        throw new Error("invalid color");
    }
    else if (stringVals.length === 4) {
        const alpha = parseFloat(stringVals.pop() || "1");
        if (isNaN(alpha)) {
            throw new Error("invalid alpha value");
        }
        alphaHex = Math.round(alpha * 255);
    }
    const vals = stringVals.map((val) => parseInt(val, 10));
    if (alphaHex !== 255) {
        vals.push(alphaHex);
    }
    return "#" + concat$1(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;
    const c = (1 - Math.abs(2 * hsla.l - 1)) * hsla.s;
    const x = c * (1 - Math.abs(((hsla.h / 60) % 2) - 1));
    const 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
    const cMin = Math.min(r, g, b);
    const cMax = Math.max(r, g, b);
    const delta = cMax - cMin;
    let h = 0;
    let s = 0;
    let l = 0;
    // Calculate hue
    // No difference
    if (delta === 0)
        h = 0;
    // Red is max
    else if (cMax === r)
        h = ((g - b) / delta) % 6;
    // Green is max
    else if (cMax === g)
        h = (b - r) / delta + 2;
    // Blue is max
    else
        h = (r - g) / delta + 4;
    h = Math.round(h * 60);
    // Make negative hues positive behind 360°
    if (h < 0)
        h += 360;
    l = (cMax + cMin) / 2;
    // Calculate saturation
    s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
    // Multiply l and s by 100
    s = +(s * 100).toFixed(1);
    l = +(l * 100).toFixed(1);
    return { a: rgba.a, h, s, l };
}
function hslaToHex(hsla) {
    return rgbaToHex(hslaToRGBA(hsla));
}
function hexToHSLA(hex) {
    return rgbaToHSLA(colorToRGBA(hex));
}
/**
 * Blend color2 on top of color1, with alpha blending.
 */
function blendColors(color1, color2) {
    const rgba2 = colorToRGBA(color2);
    const rgba1 = colorToRGBA(color1);
    const a = rgba2.a + rgba1.a * (1 - rgba2.a);
    const r = Math.round((rgba2.r * rgba2.a + rgba1.r * rgba1.a * (1 - rgba2.a)) / a);
    const g = Math.round((rgba2.g * rgba2.a + rgba1.g * rgba1.a * (1 - rgba2.a)) / a);
    const b = Math.round((rgba2.b * rgba2.a + rgba1.b * rgba1.a * (1 - rgba2.a)) / a);
    return rgbaToHex({ r, g, b, a });
}
function colorOrNumberToRGBA(color) {
    if (typeof color === "number") {
        return colorToRGBA(colorNumberToHex(color));
    }
    return colorToRGBA(color);
}
/**
 * Will compare two color strings
 * A tolerance can be provided to account for small differences that could
 * be introduced by non-bijective transformations between color spaces.
 *
 * E.g. HSV <-> RGB is not a bijection
 *
 * Note that the tolerance is applied on the euclidean distance between
 * the two **normalized** color values.
 */
function isSameColor(color1, color2, tolerance = 0) {
    if (!(isColorValid(color1) && isColorValid(color2))) {
        return false;
    }
    const rgb1 = colorToRGBA(color1);
    const rgb2 = colorToRGBA(color2);
    // alpha cannot differ as it is not impacted by transformations
    if (rgb1.a !== rgb2.a) {
        return false;
    }
    const diff = Math.sqrt(((rgb1.r - rgb2.r) / 255) ** 2 + ((rgb1.g - rgb2.g) / 255) ** 2 + ((rgb1.b - rgb2.b) / 255) ** 2);
    return diff <= tolerance;
}
function setColorAlpha(color, alpha) {
    return alpha === 1 ? toHex(color).slice(0, 7) : rgbaToHex({ ...colorToRGBA(color), a: alpha });
}
function lightenColor(color, percentage) {
    const hsla = hexToHSLA(color);
    if (percentage === 1) {
        return "#fff";
    }
    hsla.l = percentage * (100 - hsla.l) + hsla.l;
    return hslaToHex(hsla);
}
function darkenColor(color, percentage) {
    const hsla = hexToHSLA(color);
    if (percentage === 1) {
        return "#000";
    }
    // increase saturation to compensate and make it more vivid
    hsla.s = Math.min(100, percentage * hsla.s + hsla.s);
    hsla.l = hsla.l - percentage * hsla.l;
    return hslaToHex(hsla);
}
function chipTextColor(chipBackgroundColor) {
    return relativeLuminance(chipBackgroundColor) < 0.6
        ? lightenColor(chipBackgroundColor, 0.9)
        : darkenColor(chipBackgroundColor, 0.75);
}
const COLORS_SM = [
    "#4EA7F2", // Blue
    "#EA6175", // Red
    "#43C5B1", // Teal
    "#F4A261", // Orange
    "#8481DD", // Purple
    "#FFD86D", // Yellow
];
const COLORS_MD = [
    "#4EA7F2", // Blue #1
    "#3188E6", // Blue #2
    "#43C5B1", // Teal #1
    "#00A78D", // Teal #2
    "#EA6175", // Red #1
    "#CE4257", // Red #2
    "#F4A261", // Orange #1
    "#F48935", // Orange #2
    "#8481DD", // Purple #1
    "#5752D1", // Purple #2
    "#FFD86D", // Yellow #1
    "#FFBC2C", // Yellow #2
];
const COLORS_LG = [
    "#4EA7F2", // Blue #1
    "#3188E6", // Blue #2
    "#056BD9", // Blue #3
    "#A76DBC", // Violet #1
    "#7F4295", // Violet #2
    "#6D2387", // Violet #3
    "#EA6175", // Red #1
    "#CE4257", // Red #2
    "#982738", // Red #3
    "#43C5B1", // Teal #1
    "#00A78D", // Teal #2
    "#0E8270", // Teal #3
    "#F4A261", // Orange #1
    "#F48935", // Orange #2
    "#BE5D10", // Orange #3
    "#8481DD", // Purple #1
    "#5752D1", // Purple #2
    "#3A3580", // Purple #3
    "#A4A8B6", // Gray #1
    "#7E8290", // Gray #2
    "#545B70", // Gray #3
    "#FFD86D", // Yellow #1
    "#FFBC2C", // Yellow #2
    "#C08A16", // Yellow #3
];
const COLORS_XL = [
    "#4EA7F2", // Blue #1
    "#3188E6", // Blue #2
    "#056BD9", // Blue #3
    "#155193", // Blue #4
    "#A76DBC", // Violet #1
    "#7F4295", // Violet #2
    "#6D2387", // Violet #3
    "#4F1565", // Violet #4
    "#EA6175", // Red #1
    "#CE4257", // Red #2
    "#982738", // Red #3
    "#791B29", // Red #4
    "#43C5B1", // Teal #1
    "#00A78D", // Teal #2
    "#0E8270", // Teal #3
    "#105F53", // Teal #4
    "#F4A261", // Orange #1
    "#F48935", // Orange #2
    "#BE5D10", // Orange #3
    "#7D380D", // Orange #4
    "#8481DD", // Purple #1
    "#5752D1", // Purple #2
    "#3A3580", // Purple #3
    "#26235F", // Purple #4
    "#A4A8B6", // Grey #1
    "#7E8290", // Grey #2
    "#545B70", // Grey #3
    "#3F4250", // Grey #4
    "#FFD86D", // Yellow #1
    "#FFBC2C", // Yellow #2
    "#C08A16", // Yellow #3
    "#936A12", // Yellow #4
];
// Same as above but with alternating colors
const ALTERNATING_COLORS_MD = [
    "#4EA7F2", // Blue    #1
    "#43C5B1", // Teal    #1
    "#EA6175", // Red     #1
    "#F4A261", // Orange  #1
    "#8481DD", // Purple  #1
    "#FFD86D", // Yellow  #1
    "#3188E6", // Blue    #2
    "#00A78D", // Teal    #2
    "#CE4257", // Red     #2
    "#F48935", // Orange  #2
    "#5752D1", // Purple  #2
    "#FFBC2C", // Yellow  #2
];
const ALTERNATING_COLORS_LG = [
    "#4EA7F2", // Blue    #1
    "#A76DBC", // Violet  #1
    "#EA6175", // Red     #1
    "#43C5B1", // Teal    #1
    "#F4A261", // Orange  #1
    "#8481DD", // Purple  #1
    "#A4A8B6", // Gray    #1
    "#FFD86D", // Yellow  #1
    "#3188E6", // Blue    #2
    "#7F4295", // Violet  #2
    "#CE4257", // Red     #2
    "#00A78D", // Teal    #2
    "#F48935", // Orange  #2
    "#5752D1", // Purple  #2
    "#7E8290", // Gray    #2
    "#FFBC2C", // Yellow  #2
    "#056BD9", // Blue    #3
    "#6D2387", // Violet  #3
    "#982738", // Red     #3
    "#0E8270", // Teal    #3
    "#BE5D10", // Orange  #3
    "#3A3580", // Purple  #3
    "#545B70", // Gray    #3
    "#C08A16", // Yellow  #3
];
const ALTERNATING_COLORS_XL = [
    "#4EA7F2", // Blue    #1
    "#A76DBC", // Violet  #1
    "#EA6175", // Red     #1
    "#43C5B1", // Teal    #1
    "#F4A261", // Orange  #1
    "#8481DD", // Purple  #1
    "#A4A8B6", // Grey    #1
    "#FFD86D", // Yellow  #1
    "#3188E6", // Blue    #2
    "#7F4295", // Violet  #2
    "#CE4257", // Red     #2
    "#00A78D", // Teal    #2
    "#F48935", // Orange  #2
    "#5752D1", // Purple  #2
    "#7E8290", // Grey    #2
    "#FFBC2C", // Yellow  #2
    "#056BD9", // Blue    #3
    "#6D2387", // Violet  #3
    "#982738", // Red     #3
    "#0E8270", // Teal    #3
    "#BE5D10", // Orange  #3
    "#3A3580", // Purple  #3
    "#545B70", // Grey    #3
    "#C08A16", // Yellow  #3
    "#155193", // Blue    #4
    "#4F1565", // Violet  #4
    "#791B29", // Red     #4
    "#105F53", // Teal    #4
    "#7D380D", // Orange  #4
    "#26235F", // Purple  #4
    "#3F4250", // Grey    #4
    "#936A12", // Yellow  #4
];
function getNthColor(index, palette) {
    return palette[index % palette.length];
}
function getColorsPalette(quantity) {
    if (quantity <= 6) {
        return COLORS_SM;
    }
    else if (quantity <= 12) {
        return COLORS_MD;
    }
    else if (quantity <= 24) {
        return COLORS_LG;
    }
    else {
        return COLORS_XL;
    }
}
function getAlternatingColorsPalette(quantity) {
    if (quantity <= 6) {
        return COLORS_SM;
    }
    else if (quantity <= 12) {
        return ALTERNATING_COLORS_MD;
    }
    else if (quantity <= 24) {
        return ALTERNATING_COLORS_LG;
    }
    else {
        return ALTERNATING_COLORS_XL;
    }
}
class ColorGenerator {
    preferredColors;
    currentColorIndex = 0;
    palette;
    constructor(paletteSize, preferredColors = []) {
        this.preferredColors = preferredColors;
        this.palette = getColorsPalette(paletteSize).filter((c) => !preferredColors.includes(c));
    }
    next() {
        return this.preferredColors?.[this.currentColorIndex]
            ? this.preferredColors[this.currentColorIndex++]
            : getNthColor(this.currentColorIndex++, this.palette);
    }
}
class AlternatingColorGenerator extends ColorGenerator {
    constructor(paletteSize, preferredColors = []) {
        super(paletteSize, preferredColors);
        this.palette = getAlternatingColorsPalette(paletteSize).filter((c) => !preferredColors.includes(c));
    }
}
class AlternatingColorMap {
    availableColors;
    colors = {};
    constructor(paletteSize = 12) {
        this.availableColors = new AlternatingColorGenerator(paletteSize);
    }
    get(id) {
        if (!this.colors[id]) {
            this.colors[id] = this.availableColors.next();
        }
        return this.colors[id];
    }
}
/**
 * Returns a function that maps a value to a color using a color scale defined by the given
 * color/threshold values pairs.
 */
function getColorScale(colorScalePoints) {
    if (colorScalePoints.length < 2) {
        throw new Error("Color scale must have at least 2 points");
    }
    const sortedColorScalePoints = [...colorScalePoints.sort((a, b) => a.value - b.value)];
    const thresholds = [];
    for (let i = 1; i < sortedColorScalePoints.length; i++) {
        const minColorAlpha = colorOrNumberToRGBA(sortedColorScalePoints[i - 1].color).a;
        const maxColorAlpha = colorOrNumberToRGBA(sortedColorScalePoints[i].color).a;
        const minColor = colorToNumber(sortedColorScalePoints[i - 1].color);
        const maxColor = colorToNumber(sortedColorScalePoints[i].color);
        thresholds.push({
            min: sortedColorScalePoints[i - 1].value,
            max: sortedColorScalePoints[i].value,
            minColor,
            maxColor,
            minColorAlpha: minColorAlpha,
            maxColorAlpha: maxColorAlpha,
            colorDiff: computeColorDiffUnits(sortedColorScalePoints[i - 1].value, sortedColorScalePoints[i].value, minColor, maxColor),
        });
    }
    return (value) => {
        if (value < thresholds[0].min) {
            return colorNumberToHex(thresholds[0].minColor, thresholds[0].minColorAlpha);
        }
        for (const threshold of thresholds) {
            if (value >= threshold.min && value <= threshold.max) {
                return colorNumberToHex(colorCell(value, threshold.min, threshold.minColor, threshold.colorDiff), threshold.maxColorAlpha);
            }
        }
        return colorNumberToHex(thresholds[thresholds.length - 1].maxColor, thresholds[thresholds.length - 1].maxColorAlpha);
    };
}
function 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];
}
function 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;
}

//------------------------------------------------------------------------------
// Coordinate
//------------------------------------------------------------------------------
/**
 * Convert a (col) number to the corresponding letter.
 *
 * Examples:
 *     0 => 'A'
 *     25 => 'Z'
 *     26 => 'AA'
 *     27 => 'AB'
 */
function numberToLetters(n) {
    if (n < 0) {
        throw new Error(`number must be positive. Got ${n}`);
    }
    if (n < 26) {
        return String.fromCharCode(65 + n);
    }
    else {
        return numberToLetters(Math.floor(n / 26) - 1) + numberToLetters(n % 26);
    }
}
function lettersToNumber(letters) {
    let result = 0;
    const l = letters.length;
    for (let i = 0; i < l; i++) {
        const colIndex = charToNumber(letters[i]);
        result = result * 26 + colIndex;
    }
    return result - 1;
}
function charToNumber(char) {
    const charCode = char.charCodeAt(0);
    return charCode >= 65 && charCode <= 90 ? charCode - 64 : charCode - 96;
}
function isCharALetter(char) {
    return (char >= "A" && char <= "Z") || (char >= "a" && char <= "z");
}
function isCharADigit(char) {
    return char >= "0" && char <= "9";
}
// we limit the max column to 3 letters and max row to 7 digits for performance reasons
const MAX_COL = lettersToNumber("ZZZ");
const MAX_ROW = 9999998;
function consumeSpaces(chars) {
    while (chars.current === " ") {
        chars.advanceBy(1);
    }
}
function consumeLetters(chars) {
    if (chars.current === "$")
        chars.advanceBy(1);
    if (!chars.current || !isCharALetter(chars.current)) {
        return -1;
    }
    let colCoordinate = 0;
    while (chars.current && isCharALetter(chars.current)) {
        colCoordinate = colCoordinate * 26 + charToNumber(chars.shift());
    }
    return colCoordinate;
}
function consumeDigits(chars) {
    if (chars.current === "$")
        chars.advanceBy(1);
    if (!chars.current || !isCharADigit(chars.current)) {
        return -1;
    }
    let num = 0;
    while (chars.current && isCharADigit(chars.current)) {
        num = num * 10 + Number(chars.shift());
    }
    return num;
}
/**
 * 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) {
    const chars = new TokenizingChars(xc);
    consumeSpaces(chars);
    const letterPart = consumeLetters(chars);
    if (letterPart === -1 || !chars.current) {
        throw new Error(`Invalid cell description: ${xc}`);
    }
    const num = consumeDigits(chars);
    consumeSpaces(chars);
    const col = letterPart - 1;
    const row = num - 1;
    if (!chars.isOver() || col > MAX_COL || row > MAX_ROW) {
        throw new Error(`Invalid cell description: ${xc}`);
    }
    return { col, row };
}
/**
 * Convert from cartesian coordinate to the "XC" coordinate system.
 *
 * Examples:
 *   - 0,0 => A1
 *   - 1,2 => B3
 *   - 0,0, {colFixed: false, rowFixed: true} => A$1
 *   - 1,2, {colFixed: true, rowFixed: false} => $B3
 */
function toXC(col, row, rangePart = { colFixed: false, rowFixed: false }) {
    return ((rangePart.colFixed ? "$" : "") +
        numberToLetters(col) +
        (rangePart.rowFixed ? "$" : "") +
        String(row + 1));
}

const defaultTranslate = (s) => s;
const defaultLoaded = () => false;
let _translate = defaultTranslate;
let _loaded = defaultLoaded;
function sprintf(s, ...values) {
    if (values.length === 1 && typeof values[0] === "object" && !(values[0] instanceof String)) {
        const valuesDict = values[0];
        s = s.replace(/\%\(([^\)]+)\)s/g, (match, value) => valuesDict[value]);
    }
    else if (values.length > 0) {
        s = s.replace(/\%s/g, () => values.shift());
    }
    return s;
}
/***
 * Allow to inject a translation function from outside o-spreadsheet. This should be called before instantiating
 * a model.
 * @param tfn the function that will do the translation
 * @param loaded a function that returns true when the translation is loaded
 */
function setTranslationMethod(tfn, loaded = () => true) {
    _translate = tfn;
    _loaded = loaded;
}
/**
 * If no translation function has been set, this will mark the translation are loaded.
 *
 * By default, the translations should not be set as loaded, otherwise top-level translated constants will never be
 * translated. But if by the time the model is instantiated no custom translation function has been set, we can set
 * the default translation function as loaded so o-spreadsheet can be run in standalone with no translations.
 */
function setDefaultTranslationMethod() {
    if (_translate === defaultTranslate && _loaded === defaultLoaded) {
        _loaded = () => true;
    }
}
const _t = function (s, ...values) {
    if (!_loaded()) {
        return new LazyTranslatedString(s, values);
    }
    return sprintf(_translate(s), ...values);
};
class LazyTranslatedString extends String {
    values;
    constructor(str, values) {
        super(str);
        this.values = values;
    }
    valueOf() {
        const str = super.valueOf();
        return _loaded() ? sprintf(_translate(str), ...this.values) : sprintf(str, ...this.values);
    }
    toString() {
        return this.valueOf();
    }
}

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

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

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

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

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

function isSheetDependent(cmd) {
    return "sheetId" in cmd;
}
function isHeadersDependant(cmd) {
    return "dimension" in cmd && "sheetId" in cmd && "elements" in cmd;
}
function isTargetDependent(cmd) {
    return "target" in cmd && "sheetId" in cmd;
}
function isRangeDependant(cmd) {
    return "ranges" in cmd;
}
function isPositionDependent(cmd) {
    return "col" in cmd && "row" in cmd && "sheetId" in cmd;
}
function isZoneDependent(cmd) {
    return "sheetId" in cmd && "zone" in cmd;
}
const invalidateEvaluationCommands = new Set([
    "RENAME_SHEET",
    "DELETE_SHEET",
    "CREATE_SHEET",
    "DUPLICATE_SHEET",
    "ADD_COLUMNS_ROWS",
    "REMOVE_COLUMNS_ROWS",
    "UNDO",
    "REDO",
    "ADD_MERGE",
    "REMOVE_MERGE",
    "UPDATE_LOCALE",
    "ADD_PIVOT",
    "UPDATE_PIVOT",
    "INSERT_PIVOT",
    "RENAME_PIVOT",
    "REMOVE_PIVOT",
    "DUPLICATE_PIVOT",
]);
const invalidateChartEvaluationCommands = new Set([
    "EVALUATE_CELLS",
    "EVALUATE_CHARTS",
    "UPDATE_CELL",
    "UNHIDE_COLUMNS_ROWS",
    "HIDE_COLUMNS_ROWS",
    "GROUP_HEADERS",
    "UNGROUP_HEADERS",
    "FOLD_ALL_HEADER_GROUPS",
    "FOLD_HEADER_GROUP",
    "FOLD_HEADER_GROUPS_IN_ZONE",
    "UNFOLD_ALL_HEADER_GROUPS",
    "UNFOLD_HEADER_GROUP",
    "UNFOLD_HEADER_GROUPS_IN_ZONE",
    "UPDATE_TABLE",
    "UPDATE_FILTER",
    "UNDO",
    "REDO",
]);
const invalidateDependenciesCommands = new Set(["MOVE_RANGES"]);
const invalidateCFEvaluationCommands = new Set([
    "EVALUATE_CELLS",
    "ADD_CONDITIONAL_FORMAT",
    "REMOVE_CONDITIONAL_FORMAT",
    "CHANGE_CONDITIONAL_FORMAT_PRIORITY",
]);
const invalidateBordersCommands = new Set([
    "AUTOFILL_CELL",
    "SET_BORDER",
    "SET_ZONE_BORDERS",
    "SET_BORDERS_ON_TARGET",
]);
const invalidSubtotalFormulasCommands = new Set([
    "UNHIDE_COLUMNS_ROWS",
    "HIDE_COLUMNS_ROWS",
    "GROUP_HEADERS",
    "UNGROUP_HEADERS",
    "FOLD_ALL_HEADER_GROUPS",
    "FOLD_HEADER_GROUP",
    "FOLD_HEADER_GROUPS_IN_ZONE",
    "UNFOLD_ALL_HEADER_GROUPS",
    "UNFOLD_HEADER_GROUP",
    "UNFOLD_HEADER_GROUPS_IN_ZONE",
    "UPDATE_TABLE",
    "UPDATE_FILTER",
]);
const readonlyAllowedCommands = new Set([
    "START",
    "ACTIVATE_SHEET",
    "COPY",
    "RESIZE_SHEETVIEW",
    "SET_VIEWPORT_OFFSET",
    "EVALUATE_CELLS",
    "EVALUATE_CHARTS",
    "SET_FORMULA_VISIBILITY",
    "UPDATE_FILTER",
    "UPDATE_CHART",
    "UPDATE_CAROUSEL_ACTIVE_ITEM",
    "UPDATE_PIVOT",
]);
const coreTypes = new Set([
    /** CELLS */
    "UPDATE_CELL",
    "UPDATE_CELL_POSITION",
    "CLEAR_CELL",
    "CLEAR_CELLS",
    "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",
    "COLOR_SHEET",
    "HIDE_SHEET",
    "SHOW_SHEET",
    /** RANGES MANIPULATION */
    "MOVE_RANGES",
    /** CONDITIONAL FORMAT */
    "ADD_CONDITIONAL_FORMAT",
    "REMOVE_CONDITIONAL_FORMAT",
    "CHANGE_CONDITIONAL_FORMAT_PRIORITY",
    /** FIGURES */
    "CREATE_FIGURE",
    "DELETE_FIGURE",
    "UPDATE_FIGURE",
    "CREATE_CAROUSEL",
    "UPDATE_CAROUSEL",
    /** FORMATTING */
    "SET_FORMATTING",
    "CLEAR_FORMATTING",
    "SET_BORDER",
    "SET_ZONE_BORDERS",
    "SET_BORDERS_ON_TARGET",
    /** CHART */
    "CREATE_CHART",
    "UPDATE_CHART",
    "DELETE_CHART",
    /** FILTERS */
    "CREATE_TABLE",
    "REMOVE_TABLE",
    "UPDATE_TABLE",
    "CREATE_TABLE_STYLE",
    "REMOVE_TABLE_STYLE",
    /** IMAGE */
    "CREATE_IMAGE",
    /** HEADER GROUP */
    "GROUP_HEADERS",
    "UNGROUP_HEADERS",
    "UNFOLD_HEADER_GROUP",
    "FOLD_HEADER_GROUP",
    "FOLD_ALL_HEADER_GROUPS",
    "UNFOLD_ALL_HEADER_GROUPS",
    "UNFOLD_HEADER_GROUPS_IN_ZONE",
    "FOLD_HEADER_GROUPS_IN_ZONE",
    /** DATA VALIDATION */
    "ADD_DATA_VALIDATION_RULE",
    "REMOVE_DATA_VALIDATION_RULE",
    /** MISC */
    "UPDATE_LOCALE",
    /** PIVOT */
    "ADD_PIVOT",
    "UPDATE_PIVOT",
    "INSERT_PIVOT",
    "RENAME_PIVOT",
    "REMOVE_PIVOT",
    "DUPLICATE_PIVOT",
]);
function isCoreCommand(cmd) {
    return coreTypes.has(cmd.type);
}
function canExecuteInReadonly(cmd) {
    return readonlyAllowedCommands.has(cmd.type);
}
/**
 * Holds the result of a command dispatch.
 * The command may have been successfully dispatched or cancelled
 * for one or more reasons.
 */
class DispatchResult {
    reasons;
    constructor(results = []) {
        if (!Array.isArray(results)) {
            results = [results];
        }
        results = [...new Set(results)];
        this.reasons = results.filter((result) => result !== "Success" /* CommandResult.Success */);
    }
    /**
     * Static helper which returns a successful DispatchResult
     */
    static get Success() {
        return SUCCESS;
    }
    get isSuccessful() {
        return this.reasons.length === 0;
    }
    /**
     * Check if the dispatch has been cancelled because of
     * the given reason.
     */
    isCancelledBecause(reason) {
        return this.reasons.includes(reason);
    }
}
const SUCCESS = new DispatchResult();
var CommandResult;
(function (CommandResult) {
    CommandResult["Success"] = "Success";
    CommandResult["CancelledForUnknownReason"] = "CancelledForUnknownReason";
    CommandResult["WillRemoveExistingMerge"] = "WillRemoveExistingMerge";
    CommandResult["CannotMoveTableHeader"] = "CannotMoveTableHeader";
    CommandResult["MergeIsDestructive"] = "MergeIsDestructive";
    CommandResult["CellIsMerged"] = "CellIsMerged";
    CommandResult["InvalidTarget"] = "InvalidTarget";
    CommandResult["EmptyUndoStack"] = "EmptyUndoStack";
    CommandResult["EmptyRedoStack"] = "EmptyRedoStack";
    CommandResult["NotEnoughElements"] = "NotEnoughElements";
    CommandResult["NotEnoughSheets"] = "NotEnoughSheets";
    CommandResult["MissingSheetName"] = "MissingSheetName";
    CommandResult["UnchangedSheetName"] = "UnchangedSheetName";
    CommandResult["DuplicatedSheetName"] = "DuplicatedSheetName";
    CommandResult["DuplicatedSheetId"] = "DuplicatedSheetId";
    CommandResult["ForbiddenCharactersInSheetName"] = "ForbiddenCharactersInSheetName";
    CommandResult["WrongSheetMove"] = "WrongSheetMove";
    CommandResult["WrongSheetPosition"] = "WrongSheetPosition";
    CommandResult["InvalidAnchorZone"] = "InvalidAnchorZone";
    CommandResult["SelectionOutOfBound"] = "SelectionOutOfBound";
    CommandResult["TargetOutOfSheet"] = "TargetOutOfSheet";
    CommandResult["WrongCutSelection"] = "WrongCutSelection";
    CommandResult["WrongPasteSelection"] = "WrongPasteSelection";
    CommandResult["WrongPasteOption"] = "WrongPasteOption";
    CommandResult["WrongFigurePasteOption"] = "WrongFigurePasteOption";
    CommandResult["EmptyClipboard"] = "EmptyClipboard";
    CommandResult["EmptyRange"] = "EmptyRange";
    CommandResult["InvalidRange"] = "InvalidRange";
    CommandResult["InvalidZones"] = "InvalidZones";
    CommandResult["InvalidSheetId"] = "InvalidSheetId";
    CommandResult["InvalidCellId"] = "InvalidCellId";
    CommandResult["InvalidFigureId"] = "InvalidFigureId";
    CommandResult["InputAlreadyFocused"] = "InputAlreadyFocused";
    CommandResult["MaximumRangesReached"] = "MaximumRangesReached";
    CommandResult["MinimumRangesReached"] = "MinimumRangesReached";
    CommandResult["InvalidChartDefinition"] = "InvalidChartDefinition";
    CommandResult["InvalidDataSet"] = "InvalidDataSet";
    CommandResult["InvalidLabelRange"] = "InvalidLabelRange";
    CommandResult["InvalidScorecardKeyValue"] = "InvalidScorecardKeyValue";
    CommandResult["InvalidScorecardBaseline"] = "InvalidScorecardBaseline";
    CommandResult["InvalidGaugeDataRange"] = "InvalidGaugeDataRange";
    CommandResult["EmptyGaugeRangeMin"] = "EmptyGaugeRangeMin";
    CommandResult["GaugeRangeMinNaN"] = "GaugeRangeMinNaN";
    CommandResult["EmptyGaugeRangeMax"] = "EmptyGaugeRangeMax";
    CommandResult["GaugeRangeMaxNaN"] = "GaugeRangeMaxNaN";
    CommandResult["GaugeLowerInflectionPointNaN"] = "GaugeLowerInflectionPointNaN";
    CommandResult["GaugeUpperInflectionPointNaN"] = "GaugeUpperInflectionPointNaN";
    CommandResult["InvalidAutofillSelection"] = "InvalidAutofillSelection";
    CommandResult["MinBiggerThanMax"] = "MinBiggerThanMax";
    CommandResult["LowerBiggerThanUpper"] = "LowerBiggerThanUpper";
    CommandResult["MidBiggerThanMax"] = "MidBiggerThanMax";
    CommandResult["MinBiggerThanMid"] = "MinBiggerThanMid";
    CommandResult["FirstArgMissing"] = "FirstArgMissing";
    CommandResult["SecondArgMissing"] = "SecondArgMissing";
    CommandResult["MinNaN"] = "MinNaN";
    CommandResult["MidNaN"] = "MidNaN";
    CommandResult["MaxNaN"] = "MaxNaN";
    CommandResult["ValueUpperInflectionNaN"] = "ValueUpperInflectionNaN";
    CommandResult["ValueLowerInflectionNaN"] = "ValueLowerInflectionNaN";
    CommandResult["MinInvalidFormula"] = "MinInvalidFormula";
    CommandResult["MidInvalidFormula"] = "MidInvalidFormula";
    CommandResult["MaxInvalidFormula"] = "MaxInvalidFormula";
    CommandResult["ValueUpperInvalidFormula"] = "ValueUpperInvalidFormula";
    CommandResult["ValueLowerInvalidFormula"] = "ValueLowerInvalidFormula";
    CommandResult["InvalidSortAnchor"] = "InvalidSortAnchor";
    CommandResult["InvalidSortZone"] = "InvalidSortZone";
    CommandResult["SortZoneWithArrayFormulas"] = "SortZoneWithArrayFormulas";
    CommandResult["WaitingSessionConfirmation"] = "WaitingSessionConfirmation";
    CommandResult["MergeOverlap"] = "MergeOverlap";
    CommandResult["TooManyHiddenElements"] = "TooManyHiddenElements";
    CommandResult["Readonly"] = "Readonly";
    CommandResult["InvalidViewportSize"] = "InvalidViewportSize";
    CommandResult["InvalidScrollingDirection"] = "InvalidScrollingDirection";
    CommandResult["ViewportScrollLimitsReached"] = "ViewportScrollLimitsReached";
    CommandResult["FigureDoesNotExist"] = "FigureDoesNotExist";
    CommandResult["InvalidConditionalFormatId"] = "InvalidConditionalFormatId";
    CommandResult["InvalidCellPopover"] = "InvalidCellPopover";
    CommandResult["EmptyTarget"] = "EmptyTarget";
    CommandResult["InvalidFreezeQuantity"] = "InvalidFreezeQuantity";
    CommandResult["FrozenPaneOverlap"] = "FrozenPaneOverlap";
    CommandResult["ValuesNotChanged"] = "ValuesNotChanged";
    CommandResult["InvalidFilterZone"] = "InvalidFilterZone";
    CommandResult["TableNotFound"] = "TableNotFound";
    CommandResult["TableOverlap"] = "TableOverlap";
    CommandResult["InvalidTableConfig"] = "InvalidTableConfig";
    CommandResult["InvalidTableStyle"] = "InvalidTableStyle";
    CommandResult["FilterNotFound"] = "FilterNotFound";
    CommandResult["MergeInTable"] = "MergeInTable";
    CommandResult["NonContinuousTargets"] = "NonContinuousTargets";
    CommandResult["DuplicatedFigureId"] = "DuplicatedFigureId";
    CommandResult["InvalidSelectionStep"] = "InvalidSelectionStep";
    CommandResult["DuplicatedChartId"] = "DuplicatedChartId";
    CommandResult["ChartDoesNotExist"] = "ChartDoesNotExist";
    CommandResult["InvalidHeaderIndex"] = "InvalidHeaderIndex";
    CommandResult["InvalidQuantity"] = "InvalidQuantity";
    CommandResult["MoreThanOneColumnSelected"] = "MoreThanOneColumnSelected";
    CommandResult["EmptySplitSeparator"] = "EmptySplitSeparator";
    CommandResult["SplitWillOverwriteContent"] = "SplitWillOverwriteContent";
    CommandResult["NoSplitSeparatorInSelection"] = "NoSplitSeparatorInSelection";
    CommandResult["NoActiveSheet"] = "NoActiveSheet";
    CommandResult["InvalidLocale"] = "InvalidLocale";
    CommandResult["MoreThanOneRangeSelected"] = "MoreThanOneRangeSelected";
    CommandResult["NoColumnsProvided"] = "NoColumnsProvided";
    CommandResult["ColumnsNotIncludedInZone"] = "ColumnsNotIncludedInZone";
    CommandResult["DuplicatesColumnsSelected"] = "DuplicatesColumnsSelected";
    CommandResult["InvalidHeaderGroupStartEnd"] = "InvalidHeaderGroupStartEnd";
    CommandResult["HeaderGroupAlreadyExists"] = "HeaderGroupAlreadyExists";
    CommandResult["UnknownHeaderGroup"] = "UnknownHeaderGroup";
    CommandResult["UnknownDataValidationRule"] = "UnknownDataValidationRule";
    CommandResult["UnknownDataValidationCriterionType"] = "UnknownDataValidationCriterionType";
    CommandResult["InvalidDataValidationCriterionValue"] = "InvalidDataValidationCriterionValue";
    CommandResult["InvalidNumberOfCriterionValues"] = "InvalidNumberOfCriterionValues";
    CommandResult["InvalidCopyPasteSelection"] = "InvalidCopyPasteSelection";
    CommandResult["NoChanges"] = "NoChanges";
    CommandResult["InvalidInputId"] = "InvalidInputId";
    CommandResult["SheetIsHidden"] = "SheetIsHidden";
    CommandResult["InvalidTableResize"] = "InvalidTableResize";
    CommandResult["PivotIdNotFound"] = "PivotIdNotFound";
    CommandResult["PivotInError"] = "PivotInError";
    CommandResult["EmptyName"] = "EmptyName";
    CommandResult["ValueCellIsInvalidFormula"] = "ValueCellIsInvalidFormula";
    CommandResult["InvalidDefinition"] = "InvalidDefinition";
    CommandResult["InvalidColor"] = "InvalidColor";
    CommandResult["InvalidPivotDataSet"] = "InvalidPivotDataSet";
    CommandResult["InvalidPivotCustomField"] = "InvalidPivotCustomField";
    CommandResult["MissingFigureArguments"] = "MissingFigureArguments";
    CommandResult["InvalidCarouselItem"] = "InvalidCarouselItem";
})(CommandResult || (CommandResult = {}));

const availableConditionalFormatOperators = new Set([
    "containsText",
    "notContainsText",
    "isGreaterThan",
    "isGreaterOrEqualTo",
    "isLessThan",
    "isLessOrEqualTo",
    "isBetween",
    "isNotBetween",
    "beginsWithText",
    "endsWithText",
    "isNotEmpty",
    "isEmpty",
    "isNotEqual",
    "isEqual",
    "customFormula",
]);

const availableDataValidationOperators = new Set([
    "containsText",
    "notContainsText",
    "isEqualText",
    "isEmail",
    "isLink",
    "dateIs",
    "dateIsBefore",
    "dateIsOnOrBefore",
    "dateIsAfter",
    "dateIsOnOrAfter",
    "dateIsBetween",
    "dateIsNotBetween",
    "dateIsValid",
    "isEqual",
    "isNotEqual",
    "isGreaterThan",
    "isGreaterOrEqualTo",
    "isLessThan",
    "isLessOrEqualTo",
    "isBetween",
    "isNotBetween",
    "isBoolean",
    "isValueInList",
    "isValueInRange",
    "customFormula",
]);

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

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

const LAYERS = {
    Background: 0,
    Highlights: 1,
    Clipboard: 2,
    Chart: 4,
    Autofill: 5,
    Selection: 6,
    Headers: 100, // ensure that we end up on  top
};
const OrderedLayers = memoize(() => Object.keys(LAYERS).sort((a, b) => LAYERS[a] - LAYERS[b]));
/**
 *
 * @param layer New layer name
 * @param priority The lower priorities are rendered first
 */
function addRenderingLayer(layer, priority) {
    if (LAYERS[layer]) {
        throw new Error(`Layer ${layer} already exists`);
    }
    LAYERS[layer] = priority;
}

const filterCriterions = [
    "containsText",
    "notContainsText",
    "isEqualText",
    "dateIs",
    "dateIsBefore",
    "dateIsOnOrBefore",
    "dateIsAfter",
    "dateIsOnOrAfter",
    "dateIsBetween",
    "dateIsNotBetween",
    "isEqual",
    "isNotEqual",
    "isGreaterThan",
    "isGreaterOrEqualTo",
    "isLessThan",
    "isLessOrEqualTo",
    "isBetween",
    "isNotBetween",
    "customFormula",
    "beginsWithText",
    "endsWithText",
    "isNotEmpty",
    "isEmpty",
];
new Set(filterCriterions);
const filterTextCriterionOperators = [
    "containsText",
    "notContainsText",
    "isEqualText",
    "isEmpty",
    "isNotEmpty",
    "beginsWithText",
    "endsWithText",
];
const filterNumberCriterionOperators = [
    "isEqual",
    "isNotEqual",
    "isGreaterThan",
    "isGreaterOrEqualTo",
    "isLessThan",
    "isLessOrEqualTo",
    "isBetween",
    "isNotBetween",
    "isEmpty",
    "isNotEmpty",
];
const filterDateCriterionOperators = [
    "dateIs",
    "dateIsBefore",
    "dateIsOnOrBefore",
    "dateIsAfter",
    "dateIsOnOrAfter",
    "dateIsBetween",
    "dateIsNotBetween",
    "isEmpty",
    "isNotEmpty",
];

const CellErrorType = {
    NotAvailable: "#N/A",
    InvalidReference: "#REF",
    BadExpression: "#BAD_EXPR",
    CircularDependency: "#CYCLE",
    UnknownFunction: "#NAME?",
    DivisionByZero: "#DIV/0!",
    SpilledBlocked: "#SPILL!",
    GenericError: "#ERROR",
    NullError: "#NULL!",
};
const errorTypes = new Set(Object.values(CellErrorType));
class EvaluationError {
    message;
    value;
    constructor(message = _t("Error"), value = CellErrorType.GenericError) {
        this.message = message;
        this.value = value;
        this.message = message.toString();
    }
}
class BadExpressionError extends EvaluationError {
    constructor(message = _t("Invalid expression")) {
        super(message, CellErrorType.BadExpression);
    }
}
class CircularDependencyError extends EvaluationError {
    constructor(message = _t("Circular reference")) {
        super(message, CellErrorType.CircularDependency);
    }
}
class InvalidReferenceError extends EvaluationError {
    constructor(message = _t("Invalid reference")) {
        super(message, CellErrorType.InvalidReference);
    }
}
class NotAvailableError extends EvaluationError {
    constructor(message = _t("Data not available")) {
        super(message, CellErrorType.NotAvailable);
    }
}
class UnknownFunctionError extends EvaluationError {
    constructor(message = _t("Unknown function")) {
        super(message, CellErrorType.UnknownFunction);
    }
}
class SplillBlockedError extends EvaluationError {
    constructor(message = _t("Spill range is not empty")) {
        super(message, CellErrorType.SpilledBlocked);
    }
}
class DivisionByZeroError extends EvaluationError {
    constructor(message = _t("Division by zero")) {
        super(message, CellErrorType.DivisionByZero);
    }
}

// HELPERS
const SORT_TYPES_ORDER = ["number", "string", "boolean", "undefined"];
function inferFormat(data) {
    if (data === undefined) {
        return undefined;
    }
    if (isMatrix(data)) {
        return data[0][0]?.format;
    }
    return data.format;
}
function isEvaluationError(error) {
    return typeof error === "string" && errorTypes.has(error);
}
function valueNotAvailable(searchKey) {
    return {
        value: CellErrorType.NotAvailable,
        message: _t("Did not find value '%s' in [[FUNCTION_NAME]] evaluation.", toString(searchKey)),
    };
}
// -----------------------------------------------------------------------------
// FORMAT FUNCTIONS
// -----------------------------------------------------------------------------
const expectNumberValueError = (value) => _t("The function [[FUNCTION_NAME]] expects a number value, but '%s' is a string, and cannot be coerced to a number.", value);
const expectNumberRangeError = (lowerBound, upperBound, value) => _t("The function [[FUNCTION_NAME]] expects a number value between %s and %s inclusive, but receives %s.", lowerBound.toString(), upperBound.toString(), value.toString());
const expectStringSetError = (stringSet, value) => {
    const stringSetString = stringSet.map((str) => `'${str}'`).join(", ");
    return _t("The function [[FUNCTION_NAME]] has an argument with value '%s'. It should be one of: %s.", value, stringSetString);
};
function toNumber(data, locale) {
    const value = toValue(data);
    switch (typeof value) {
        case "number":
            return value;
        case "boolean":
            return value ? 1 : 0;
        case "string":
            if (isNumber(value, locale) || value === "") {
                return parseNumber(value, locale);
            }
            const internalDate = parseDateTime(value, locale);
            if (internalDate) {
                return internalDate.value;
            }
            throw new EvaluationError(expectNumberValueError(value));
        default:
            return 0;
    }
}
function tryToNumber(value, locale) {
    try {
        return toNumber(value, locale);
    }
    catch (e) {
        return undefined;
    }
}
function toNumberMatrix(data, argName) {
    return toMatrix(data).map((row) => {
        return row.map((cell) => {
            if (typeof cell.value !== "number") {
                let message = "";
                if (typeof cell === "object") {
                    message = _t("Function [[FUNCTION_NAME]] expects number values for %s, but got an empty value.", argName);
                }
                else if (typeof cell === "string") {
                    message = _t("Function [[FUNCTION_NAME]] expects number values for %s, but got a string.", argName);
                }
                else if (typeof cell === "boolean") {
                    message = _t("Function [[FUNCTION_NAME]] expects number values for %s, but got a boolean.", argName);
                }
                throw new EvaluationError(message);
            }
            return cell.value;
        });
    });
}
function strictToNumber(data, locale) {
    const value = toValue(data);
    if (value === "") {
        throw new EvaluationError(expectNumberValueError(value));
    }
    return toNumber(value, locale);
}
function toInteger(value, locale) {
    return Math.trunc(toNumber(value, locale));
}
function strictToInteger(value, locale) {
    return Math.trunc(strictToNumber(value, locale));
}
function toString(data) {
    const value = toValue(data);
    switch (typeof value) {
        case "string":
            return value;
        case "number":
            return value.toString();
        case "boolean":
            return value ? "TRUE" : "FALSE";
        default:
            return "";
    }
}
/** Normalize string by setting it to lowercase and replacing accent letters with plain letters */
const normalizeString = memoize(function normalizeString(str) {
    return str
        .toLowerCase()
        .normalize("NFD")
        .replace(/[\u0300-\u036f]/g, "");
});
const expectBooleanValueError = (value) => _t("The function [[FUNCTION_NAME]] expects a boolean value, but '%s' is a text, and cannot be coerced to a boolean.", value);
function toBoolean(data) {
    const value = toValue(data);
    switch (typeof value) {
        case "boolean":
            return value;
        case "string":
            if (value) {
                const uppercaseVal = value.toUpperCase();
                if (uppercaseVal === "TRUE") {
                    return true;
                }
                if (uppercaseVal === "FALSE") {
                    return false;
                }
                throw new EvaluationError(expectBooleanValueError(value));
            }
            else {
                return false;
            }
        case "number":
            return value ? true : false;
        default:
            return false;
    }
}
function strictToBoolean(data) {
    const value = toValue(data);
    if (value === "") {
        throw new EvaluationError(expectBooleanValueError(value));
    }
    return toBoolean(value);
}
function toJsDate(data, locale) {
    const value = toValue(data);
    return numberToJsDate(toNumber(value, locale));
}
function toValue(data) {
    if (typeof data === "object" && data !== null && "value" in data) {
        if (isEvaluationError(data.value)) {
            throw data;
        }
        return data.value;
    }
    if (isEvaluationError(data)) {
        throw new EvaluationError("", data);
    }
    return data;
}
// -----------------------------------------------------------------------------
// VISIT FUNCTIONS
// -----------------------------------------------------------------------------
function visitArgs(args, cellCb, dataCb) {
    for (const arg of args) {
        if (isMatrix(arg)) {
            // arg is ref to a Cell/Range
            const lenRow = arg.length;
            const lenCol = arg[0].length;
            for (let y = 0; y < lenCol; y++) {
                for (let x = 0; x < lenRow; x++) {
                    cellCb(arg[x][y]);
                }
            }
        }
        else {
            // arg is set directly in the formula function
            dataCb(arg);
        }
    }
}
function visitAny(args, cb) {
    visitArgs(args, (cell) => {
        if (isEvaluationError(cell.value)) {
            throw cell;
        }
        cb(cell);
    }, (arg) => {
        if (isEvaluationError(arg?.value)) {
            throw arg;
        }
        cb(arg);
    });
}
function visitNumbers(args, cb, locale) {
    visitArgs(args, (cell) => {
        if (typeof cell?.value === "number") {
            cb(cell);
        }
        if (isEvaluationError(cell?.value)) {
            throw cell;
        }
    }, (arg) => {
        cb({ value: strictToNumber(arg, locale), format: arg?.format });
    });
}
// -----------------------------------------------------------------------------
// REDUCE FUNCTIONS
// -----------------------------------------------------------------------------
function reduceArgs(args, cellCb, dataCb, initialValue, dir = "rowFirst") {
    let val = initialValue;
    for (const arg of args) {
        if (isMatrix(arg)) {
            // arg is ref to a Cell/Range
            const numberOfCols = arg.length;
            const numberOfRows = arg[0].length;
            if (dir === "rowFirst") {
                for (let row = 0; row < numberOfRows; row++) {
                    for (let col = 0; col < numberOfCols; col++) {
                        val = cellCb(val, arg[col][row]);
                    }
                }
            }
            else {
                for (let col = 0; col < numberOfCols; col++) {
                    for (let row = 0; row < numberOfRows; row++) {
                        val = cellCb(val, arg[col][row]);
                    }
                }
            }
        }
        else {
            // arg is set directly in the formula function
            val = dataCb(val, arg);
        }
    }
    return val;
}
function reduceAny(args, cb, initialValue, dir = "rowFirst") {
    return reduceArgs(args, cb, cb, initialValue, dir);
}
function reduceNumbers(args, cb, initialValue, locale) {
    return reduceArgs(args, (acc, arg) => {
        const argValue = arg?.value;
        if (typeof argValue === "number") {
            return cb(acc, argValue);
        }
        else if (isEvaluationError(argValue)) {
            throw arg;
        }
        return acc;
    }, (acc, arg) => {
        return cb(acc, strictToNumber(arg, locale));
    }, initialValue);
}
function reduceNumbersTextAs0(args, cb, initialValue, locale) {
    return reduceArgs(args, (acc, arg) => {
        const argValue = arg?.value;
        if (argValue !== undefined && argValue !== null) {
            if (typeof argValue === "number") {
                return cb(acc, argValue);
            }
            else if (typeof argValue === "boolean") {
                return cb(acc, toNumber(argValue, locale));
            }
            else if (isEvaluationError(argValue)) {
                throw arg;
            }
            else {
                return cb(acc, 0);
            }
        }
        return acc;
    }, (acc, arg) => {
        return cb(acc, toNumber(arg, locale));
    }, initialValue);
}
// -----------------------------------------------------------------------------
// MATRIX FUNCTIONS
// -----------------------------------------------------------------------------
/**
 * Generate a matrix of size nColumns x nRows and apply a callback on each position
 */
function generateMatrix(nColumns, nRows, callback) {
    const returned = Array(nColumns);
    for (let col = 0; col < nColumns; col++) {
        returned[col] = Array(nRows);
        for (let row = 0; row < nRows; row++) {
            returned[col][row] = callback(col, row);
        }
    }
    return returned;
}
function matrixMap(matrix, callback) {
    if (matrix.length === 0) {
        return [];
    }
    return generateMatrix(matrix.length, matrix[0].length, (col, row) => callback(matrix[col][row]));
}
function matrixForEach(matrix, fn) {
    const numberOfCols = matrix.length;
    const numberOfRows = matrix[0]?.length ?? 0;
    for (let col = 0; col < numberOfCols; col++) {
        for (let row = 0; row < numberOfRows; row++) {
            fn(matrix[col][row]);
        }
    }
}
function transposeMatrix(matrix) {
    if (!matrix.length) {
        return [];
    }
    return generateMatrix(matrix[0].length, matrix.length, (i, j) => matrix[j][i]);
}
/**
 * Enables a formula function to accept matrix or vector inputs instead of simple value, computing results across multiple dimensions.
 *
 * ```
 *                    /         |‾                 ‾| \        |‾                                                    ‾|
 *                   |          | [A]               |  |       | compute(A, D, E), compute(A, D, F), compute(A, D, G) |
 * applyVectorization| compute, | [B], D, [E, F, G] |  |  <=>  | compute(B, D, E), compute(B, D, F), compute(B, D, G) |
 *                   |          | [C]               |  |       | compute(C, D, E), compute(C, D, F), compute(C, D, G) |
 *                    \         |_                 _| /        |_                                                    _|
 * ```
 *
 * By default, all arguments are vectorized. To control which arguments are vectorized,
 * pass an `acceptToVectorize` boolean array of the same length as `args`:
 * - `true`  enables vectorization for that argument
 * - `false` disables vectorization for that argument
 *
 * For example, with `[true, true, false]` on previous example you get:
 *
 * ```
 * |‾                        ‾|
 * | compute(A, D, [E, F, G]) |
 * | compute(B, D, [E, F, G]) |
 * | compute(C, D, [E, F, G]) |
 * |_                        _|
 * ```
 *
 * @remarks
 * This helper is automatically applied (by default) to **all** `compute` functions
 * across the various spreadsheet formula modules:
 * - If an argument is declared **scalar** (not `"range"`), it is vectorized.
 * - If **all** arguments are declared **ranges**, no vectorization occurs.
 *   - e.g. `SUM(A1:B2)` returns a 1×1 sum over the range.
 *   - e.g. `COS(A1:B2)` over `A1:B2` returns a 2×2 element-wise result.
 * - For special behaviors (e.g. the `IF` function), you may declare all arguments
 *   as ranges and invoke this helper directly within your `compute` implementation.
 */
function applyVectorization(formula, args, acceptToVectorize = undefined) {
    let countVectorizedCol = 1;
    let countVectorizedRow = 1;
    let vectorizedColLimit = Infinity;
    let vectorizedRowLimit = Infinity;
    let vectorArgsType = undefined;
    for (let i = 0; i < args.length; i++) {
        const arg = args[i];
        if (isMatrix(arg) && (acceptToVectorize === undefined || acceptToVectorize[i])) {
            const nColumns = arg.length;
            const nRows = arg[0].length;
            if (nColumns !== 1 || nRows !== 1) {
                vectorArgsType ??= new Array(args.length);
                if (nColumns !== 1 && nRows !== 1) {
                    vectorArgsType[i] = "matrix";
                    countVectorizedCol = Math.max(countVectorizedCol, nColumns);
                    countVectorizedRow = Math.max(countVectorizedRow, nRows);
                    vectorizedColLimit = Math.min(vectorizedColLimit, nColumns);
                    vectorizedRowLimit = Math.min(vectorizedRowLimit, nRows);
                }
                else if (nColumns !== 1) {
                    vectorArgsType[i] = "horizontal";
                    countVectorizedCol = Math.max(countVectorizedCol, nColumns);
                    vectorizedColLimit = Math.min(vectorizedColLimit, nColumns);
                }
                else if (nRows !== 1) {
                    vectorArgsType[i] = "vertical";
                    countVectorizedRow = Math.max(countVectorizedRow, nRows);
                    vectorizedRowLimit = Math.min(vectorizedRowLimit, nRows);
                }
            }
            else {
                args[i] = arg[0][0];
            }
        }
    }
    if (countVectorizedCol === 1 && countVectorizedRow === 1) {
        // either this function is not vectorized or it ends up with a 1x1 dimension
        return formula(...args);
    }
    const getArgOffset = (i, j) => args.map((arg, index) => {
        switch (vectorArgsType?.[index]) {
            case "matrix":
                return arg[i][j];
            case "horizontal":
                return arg[i][0];
            case "vertical":
                return arg[0][j];
            case undefined:
                return arg;
        }
    });
    return generateMatrix(countVectorizedCol, countVectorizedRow, (col, row) => {
        if (col > vectorizedColLimit - 1 || row > vectorizedRowLimit - 1) {
            return new NotAvailableError(_t("Array arguments to [[FUNCTION_NAME]] are of different size."));
        }
        const singleCellComputeResult = formula(...getArgOffset(col, row));
        // In the case where the user tries to vectorize arguments of an array formula, we will get an
        // array for every combination of the vectorized arguments, which will lead to a 3D matrix and
        // we won't be able to return the values.
        // In this case, we keep the first element of each spreading part, just as Excel does, and
        // create an array with these parts.
        // For exemple, we have MUNIT(x) that return an unitary matrix of x*x. If we use it with a
        // range, like MUNIT(A1:A2), we will get two unitary matrices (one for the value in A1 and one
        // for the value in A2). In this case, we will simply take the first value of each matrix and
        // return the array [First value of MUNIT(A1), First value of MUNIT(A2)].
        return isMatrix(singleCellComputeResult)
            ? singleCellComputeResult[0][0]
            : singleCellComputeResult;
    });
}
// -----------------------------------------------------------------------------
// 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 (const arg of args) {
        if (isMatrix(arg)) {
            // arg is ref to a Cell/Range
            const lenRow = arg.length;
            const lenCol = arg[0].length;
            for (let y = 0; y < lenCol; y++) {
                for (let x = 0; x < lenRow; x++) {
                    if (!cellCb(arg[x][y] ?? undefined))
                        return;
                }
            }
        }
        else {
            // arg is set directly in the formula function
            if (!dataCb(arg))
                return;
        }
    }
}
function conditionalVisitBoolean(args, cb) {
    return conditionalVisitArgs(args, (arg) => {
        const argValue = arg?.value;
        if (typeof argValue === "boolean") {
            return cb(argValue);
        }
        if (typeof argValue === "number") {
            return cb(argValue ? true : false);
        }
        if (isEvaluationError(argValue)) {
            throw arg;
        }
        return true;
    }, (arg) => {
        if (arg !== undefined && arg.value !== null) {
            return cb(strictToBoolean(arg));
        }
        return true;
    });
}
function getPredicate(descr, locale) {
    let operator;
    let operand;
    let subString = descr.substring(0, 2);
    if (subString === "<=" || subString === ">=" || subString === "<>") {
        operator = subString;
        operand = descr.substring(2);
    }
    else {
        subString = descr.substring(0, 1);
        if (subString === "<" || subString === ">" || subString === "=") {
            operator = subString;
            operand = descr.substring(1);
        }
        else {
            operator = "=";
            operand = descr;
        }
    }
    if (isNumber(operand, locale) || isDateTime(operand, locale)) {
        operand = toNumber(operand, locale);
    }
    else if (operand === "TRUE" || operand === "FALSE") {
        operand = toBoolean(operand);
    }
    return { operator, operand };
}
/**
 * Converts a search string containing wildcard characters to a regular expression.
 *
 * The function iterates over each character in the input string. If the character is a wildcard
 * character ("?" or "*") and is not preceded by a "~", it is replaced by the corresponding regular
 * expression.
 * If the character is a special regular expression character, it is escaped with "\\".
 */
const wildcardToRegExp = memoize(function wildcardToRegExp(operand) {
    if (operand === "*") {
        return /.+/;
    }
    let exp = "";
    let predecessor = "";
    for (const 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, locale) {
    const { operator, operand } = criterion;
    if (operand === undefined || value === null || operand === null) {
        return false;
    }
    if (typeof operand === "number" && operator === "=") {
        if (typeof value === "string" && (isNumber(value, locale) || isDateTime(value, locale))) {
            return toNumber(value, locale) === operand;
        }
        return value === operand;
    }
    if (operator === "<>" || operator === "=") {
        let result;
        if (typeof value === typeof operand) {
            if (value === "" && operand === "") {
                // fast path to avoid regex evaluation
                result = true;
            }
            else if (typeof value === "string" && typeof operand === "string") {
                result = wildcardToRegExp(operand).test(value);
            }
            else {
                result = value === operand;
            }
        }
        else {
            result = false;
        }
        return operator === "=" ? result : !result;
    }
    if (typeof value === typeof operand) {
        switch (operator) {
            case "<":
                return value < operand;
            case ">":
                return value > operand;
            case "<=":
                return value <= operand;
            case ">=":
                return value >= operand;
        }
    }
    return false;
}
/**
 * Functions used especially for predicate evaluation on ranges.
 *
 * Take ranges with same dimensions and take predicates, one for each range.
 * For (i, j) coordinates, if all elements with coordinates (i, j) of each
 * range correspond to the associated predicate, then the function uses a callback
 * function with the parameters "i" and "j".
 *
 * Syntax:
 * visitMatchingRanges([range1, predicate1, range2, predicate2, ...], cb(i,j), likeSelection)
 *
 * - range1 (range): The range to check against predicate1.
 * - predicate1 (string): The pattern or test to apply to range1.
 * - range2: (range, repeatable) ranges to check.
 * - predicate2 (string, repeatable): Additional pattern or test to apply to range2.
 *
 * - cb(i: number, j: number) => void: the callback function.
 *
 * - isQuery (boolean) indicates if the comparison with a string should be done as a SQL-like query.
 * (Ex1 isQuery = true, predicate = "abc", element = "abcde": predicate match the element),
 * (Ex2 isQuery = false, predicate = "abc", element = "abcde": predicate not match the element).
 * (Ex3 isQuery = true, predicate = "abc", element = "abc": predicate match the element),
 * (Ex4 isQuery = false, predicate = "abc", element = "abc": predicate match the element).
 */
function visitMatchingRanges(args, cb, locale, isQuery = false) {
    const countArg = args.length;
    if (countArg % 2 === 1) {
        throw new EvaluationError(_t("Function [[FUNCTION_NAME]] expects criteria_range and criterion to be in pairs."));
    }
    const firstArg = toMatrix(args[0]);
    const dimRow = firstArg.length;
    const dimCol = firstArg[0].length;
    const predicates = [];
    for (let i = 0; i < countArg - 1; i += 2) {
        const criteriaRange = toMatrix(args[i]);
        if (criteriaRange.length !== dimRow || criteriaRange[0].length !== dimCol) {
            throw new EvaluationError(_t("Function [[FUNCTION_NAME]] expects criteria_range to have the same dimension"));
        }
        const description = toString(args[i + 1]);
        const predicate = getPredicate(description, locale);
        if (isQuery && typeof predicate.operand === "string") {
            predicate.operand += "*";
        }
        predicates.push(predicate);
    }
    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 = toMatrix(args[k])[i][j].value;
                const criterion = predicates[k / 2];
                validatedPredicates = evaluatePredicate(criteriaValue ?? undefined, criterion, locale);
                if (!validatedPredicates) {
                    break;
                }
            }
            if (validatedPredicates) {
                cb(i, j);
            }
        }
    }
}
// -----------------------------------------------------------------------------
// COMMON FUNCTIONS
// -----------------------------------------------------------------------------
/**
 * Perform a dichotomic search on an array and return the index of the nearest match.
 *
 * The array should be sorted, if not an incorrect value might be returned. In the case where multiple
 * element of the array match the target, the method will return the first match if the array is sorted
 * in descending order, and the last match if the array is in ascending order.
 *
 *
 * @param data the array in which to search.
 * @param target the value to search.
 * @param mode "nextGreater/nextSmaller" : return next greater/smaller value if no exact match is found.
 * @param sortOrder whether the array is sorted in ascending or descending order.
 * @param rangeLength the number of elements to consider in the search array.
 * @param getValueInData function returning the element at index i in the search array.
 */
function dichotomicSearch(data, target, mode, sortOrder, rangeLength, getValueInData) {
    if (target === undefined || target.value === null) {
        return -1;
    }
    if (isEvaluationError(target.value)) {
        throw target;
    }
    const _target = normalizeValue(target.value);
    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) => normalizeValue(getValueInData(data, rangeLength - i - 1))
        : (i) => normalizeValue(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 || currentVal === null) {
            indexLeft = indexMedian + 1;
            continue;
        }
        // 2 - check if value match
        if (mode === "strict" && currentVal === _target) {
            matchVal = currentVal;
            matchValIndex = currentIndex;
        }
        else if (mode === "nextSmaller" && currentVal <= _target) {
            if (matchVal === undefined ||
                matchVal === null ||
                matchVal < currentVal ||
                (matchVal === currentVal && 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, lookupCaches, reverseSearch = false) {
    if (target === undefined || target.value === null) {
        return -1;
    }
    if (isEvaluationError(target.value)) {
        throw target;
    }
    const _target = normalizeValue(target.value);
    const getValue = reverseSearch
        ? (data, i) => normalizeValue(getValueInData(data, numberOfValues - i - 1))
        : (data, i) => normalizeValue(getValueInData(data, i));
    // first check if the target is in the cache
    const isNotWildcardTarget = mode !== "wildcard" ||
        typeof _target !== "string" ||
        !(_target.includes("*") || _target.includes("?"));
    if (lookupCaches && isNotWildcardTarget) {
        const searchMode = reverseSearch ? "reverseSearch" : "forwardSearch";
        let cache = lookupCaches[searchMode].get(data);
        if (cache === undefined) {
            // build the cache for all the values
            cache = new Map();
            for (let i = 0; i < numberOfValues; i++) {
                const value = getValue(data, i) ?? null;
                if (!cache.has(value)) {
                    cache.set(value, i);
                }
            }
            lookupCaches[searchMode].set(data, cache);
        }
        if (cache.has(_target)) {
            const resultIndex = cache.get(_target);
            return reverseSearch ? numberOfValues - resultIndex - 1 : resultIndex;
        }
        if (mode === "strict" || mode === "wildcard") {
            return -1;
        }
    }
    // else perform the linear search
    const resultIndex = _linearSearch(data, _target, mode, numberOfValues, getValue);
    return reverseSearch && resultIndex !== -1 ? numberOfValues - resultIndex - 1 : resultIndex;
}
function _linearSearch(data, _target, mode, numberOfValues, getNormalizeValue) {
    let indexMatchTarget = (i) => {
        return getNormalizeValue(data, i) === _target;
    };
    if (mode === "wildcard" &&
        typeof _target === "string" &&
        (_target.includes("*") || _target.includes("?"))) {
        const regExp = wildcardToRegExp(_target);
        indexMatchTarget = (i) => {
            const value = getNormalizeValue(data, i);
            if (typeof value === "string") {
                return regExp.test(value);
            }
            return false;
        };
    }
    let closestMatch = undefined;
    let closestMatchIndex = -1;
    if (mode === "nextSmaller") {
        indexMatchTarget = (i) => {
            const value = getNormalizeValue(data, i);
            if ((!closestMatch && compareCellValues(_target, value) >= 0) ||
                (compareCellValues(_target, value) >= 0 && compareCellValues(value, closestMatch) > 0)) {
                closestMatch = value;
                closestMatchIndex = i;
            }
            return value === _target;
        };
    }
    if (mode === "nextGreater") {
        indexMatchTarget = (i) => {
            const value = getNormalizeValue(data, i);
            if ((!closestMatch && compareCellValues(_target, value) <= 0) ||
                (compareCellValues(_target, value) <= 0 && compareCellValues(value, closestMatch) < 0)) {
                closestMatch = value;
                closestMatchIndex = i;
            }
            return value === _target;
        };
    }
    for (let i = 0; i < numberOfValues; i++) {
        if (indexMatchTarget(i)) {
            return i;
        }
    }
    return closestMatchIndex;
}
/**
 * Normalize a value.
 * If the cell value is a string, this will set it to lowercase and replacing accent letters with plain letters
 */
function normalizeValue(value) {
    return typeof value === "string" ? normalizeString(value) : value;
}
function compareCellValues(left, right) {
    let typeOrder = SORT_TYPES_ORDER.indexOf(typeof left) - SORT_TYPES_ORDER.indexOf(typeof right);
    if (typeOrder === 0) {
        if (typeof left === "string" && typeof right === "string") {
            typeOrder = left.localeCompare(right);
        }
        else if (typeof left === "number" && typeof right === "number") {
            typeOrder = left - right;
        }
        else if (typeof left === "boolean" && typeof right === "boolean") {
            typeOrder = Number(left) - Number(right);
        }
    }
    return typeOrder;
}
function toMatrix(data) {
    if (data === undefined) {
        return [[]];
    }
    return isMatrix(data) ? data : [[data]];
}
/**
 * Flatten an array of items, where each item can be a single value or a 2D array, and apply the
 * callback to each element.
 *
 * The 2D array are flattened row first.
 */
function flattenRowFirst(items, callback) {
    /**/
    return reduceAny(items, (array, val) => {
        array.push(callback(val));
        return array;
    }, [], "rowFirst");
}
function isDataNonEmpty(data) {
    if (data === undefined) {
        return false;
    }
    const { value } = data;
    if (value === null || value === "") {
        return false;
    }
    return true;
}
const noValidInputErrorMessage = _t("[[FUNCTION_NAME]] has no valid input data.");
function emptyDataErrorMessage(argName) {
    return _t("[[FUNCTION_NAME]] expects the provided values of %(argName)s to be a non-empty matrix.", { argName });
}

function tokenizeFormat(str) {
    const chars = new TokenizingChars(str);
    const result = [];
    let currentFormatPart = [];
    result.push(currentFormatPart);
    while (!chars.isOver()) {
        if (chars.current === ";") {
            currentFormatPart = [];
            result.push(currentFormatPart);
            chars.shift();
            continue;
        }
        const token = tokenizeDigit(chars) ||
            tokenizeString$1(chars) ||
            tokenizeEscapedChars(chars) ||
            tokenizeThousandsSeparator(chars) ||
            tokenizeDecimalPoint(chars) ||
            tokenizePercent(chars) ||
            tokenizeDatePart(chars) ||
            tokenizeTextPlaceholder(chars) ||
            tokenizeRepeatedChar(chars);
        if (!token) {
            throw new Error("Unknown token at " + chars.remaining());
        }
        currentFormatPart.push(token);
    }
    return result;
}
function tokenizeString$1(chars) {
    let enfOfStringChar;
    if (chars.current === '"') {
        chars.shift();
        enfOfStringChar = '"';
    }
    else if (chars.currentStartsWith("[$")) {
        chars.advanceBy(2);
        enfOfStringChar = "]";
    }
    if (!enfOfStringChar) {
        return null;
    }
    let letters = "";
    while (chars.current && chars.current !== enfOfStringChar) {
        letters += chars.shift();
    }
    if (chars.current === enfOfStringChar) {
        chars.shift();
    }
    else {
        throw new Error("Unterminated string in format");
    }
    return {
        type: "STRING",
        value: letters,
    };
}
const alwaysEscapedCharsInFormat = new Set("$+-/():!^&~{}<>= ");
function tokenizeEscapedChars(chars) {
    if (chars.current === "\\") {
        chars.shift();
        const escapedChar = chars.shift();
        if (!escapedChar) {
            throw new Error("Unexpected end of format string");
        }
        return {
            type: "CHAR",
            value: escapedChar,
        };
    }
    if (alwaysEscapedCharsInFormat.has(chars.current)) {
        return {
            type: "CHAR",
            value: chars.shift(),
        };
    }
    return null;
}
function tokenizeThousandsSeparator(chars) {
    if (chars.current === ",") {
        chars.shift();
        return { type: "THOUSANDS_SEPARATOR", value: "," };
    }
    return null;
}
function tokenizeTextPlaceholder(chars) {
    if (chars.current === "@") {
        chars.shift();
        return { type: "TEXT_PLACEHOLDER", value: "@" };
    }
    return null;
}
function tokenizeDecimalPoint(chars) {
    if (chars.current === ".") {
        chars.shift();
        return { type: "DECIMAL_POINT", value: "." };
    }
    return null;
}
function tokenizePercent(chars) {
    if (chars.current === "%") {
        chars.shift();
        return { type: "PERCENT", value: "%" };
    }
    return null;
}
function tokenizeDigit(chars) {
    if (chars.current === "0" || chars.current === "#") {
        const value = chars.current;
        chars.shift();
        return { type: "DIGIT", value };
    }
    return null;
}
const dateSymbols = new Set("dmqyhsa");
function tokenizeDatePart(chars) {
    if (!dateSymbols.has(chars.current)) {
        return null;
    }
    const char = chars.current;
    let value = "";
    while (chars.current === char) {
        value += chars.shift();
    }
    return { type: "DATE_PART", value };
}
function tokenizeRepeatedChar(chars) {
    if (chars.current !== "*") {
        return null;
    }
    chars.shift();
    const repeatedChar = chars.shift();
    if (!repeatedChar) {
        throw new Error("Unexpected end of format string");
    }
    return {
        type: "REPEATED_CHAR",
        value: repeatedChar,
    };
}

/**
 *  Constant used to indicate the maximum of digits that is possible to display
 *  in a cell with standard size.
 */
const MAX_DECIMAL_PLACES = 20;
const internalFormatCache = {};
function parseFormat(formatString) {
    let internalFormat = internalFormatCache[formatString];
    if (internalFormat === undefined) {
        internalFormat = convertFormatToInternalFormat(formatString);
        internalFormatCache[formatString] = internalFormat;
    }
    return internalFormat;
}
function convertFormatToInternalFormat(format) {
    const formatParts = tokenizeFormat(format);
    // A format can only have a single REPEATED_CHAR token. The rest are converted to simple CHAR tokens.
    for (const part of formatParts) {
        const repeatedCharTokens = part.filter((token) => token.type === "REPEATED_CHAR");
        for (const repeatedCharToken of repeatedCharTokens.slice(1)) {
            repeatedCharToken.type = "CHAR";
        }
    }
    const positiveFormat = parseDateFormatTokens(formatParts[0]) ||
        parseNumberFormatTokens(formatParts[0]) ||
        tokensToTextInternalFormat(formatParts[0]);
    if (!positiveFormat) {
        throw new Error("Invalid first format part of: " + format);
    }
    if (formatParts.length > 1 && positiveFormat.type === "text") {
        throw new Error("The first format in a multi-part format must be a number format: " + format);
    }
    const negativeFormat = parseDateFormatTokens(formatParts[1]) || parseNumberFormatTokens(formatParts[1]);
    if (formatParts[1]?.length && !negativeFormat) {
        throw new Error("Invalid second format part of: " + format);
    }
    const zeroFormat = parseDateFormatTokens(formatParts[2]) || parseNumberFormatTokens(formatParts[2]);
    if (formatParts[2]?.length && !zeroFormat) {
        throw new Error("Invalid third format part of: " + format);
    }
    const textFormat = tokensToTextInternalFormat(formatParts[3]);
    if (formatParts[3]?.length && !textFormat) {
        throw new Error("Invalid fourth format part of: " + format);
    }
    return { positive: positiveFormat, negative: negativeFormat, zero: zeroFormat, text: textFormat };
}
function areValidDateFormatTokens(tokens) {
    return tokens.every((token) => token.type === "DATE_PART" ||
        token.type === "DECIMAL_POINT" ||
        token.type === "THOUSANDS_SEPARATOR" ||
        token.type === "STRING" ||
        token.type === "CHAR" ||
        token.type === "REPEATED_CHAR");
}
function areValidNumberFormatTokens(tokens) {
    return tokens.every((token) => token.type === "DIGIT" ||
        token.type === "DECIMAL_POINT" ||
        token.type === "THOUSANDS_SEPARATOR" ||
        token.type === "PERCENT" ||
        token.type === "STRING" ||
        token.type === "CHAR" ||
        token.type === "REPEATED_CHAR");
}
function areValidTextFormatTokens(tokens) {
    return tokens.every((token) => token.type === "STRING" ||
        token.type === "TEXT_PLACEHOLDER" ||
        token.type === "CHAR" ||
        token.type === "REPEATED_CHAR");
}
function parseNumberFormatTokens(tokens) {
    if (!tokens || !areValidNumberFormatTokens(tokens)) {
        return undefined;
    }
    const integerPart = [];
    let decimalPart = undefined;
    let parsedPart = integerPart;
    let percentSymbols = 0;
    let magnitude = 0;
    let lastIndexOfDigit = tokens.findLastIndex((token) => token.type === "DIGIT");
    let hasThousandSeparator = false;
    let numberOfDecimalsDigits = 0;
    for (let i = 0; i < tokens.length; i++) {
        const token = tokens[i];
        switch (token.type) {
            case "DIGIT":
                if (parsedPart === integerPart) {
                    parsedPart.push(token);
                }
                else if (numberOfDecimalsDigits < MAX_DECIMAL_PLACES) {
                    parsedPart.push(token);
                    numberOfDecimalsDigits++;
                }
                break;
            case "DECIMAL_POINT":
                if (parsedPart === integerPart) {
                    decimalPart = [];
                    parsedPart = decimalPart;
                }
                else {
                    throw new Error("Multiple decimal points in a number format");
                }
                break;
            case "REPEATED_CHAR":
            case "CHAR":
            case "STRING":
                parsedPart.push(token);
                break;
            case "PERCENT":
                percentSymbols++;
                parsedPart.push(token);
                break;
            // Per OpenXML Spec:
            // - If a comma is between two DIGIT tokens, and in the integer part, a thousand separator is applied in the formatted value.
            // - If a comma is at the end of the number placeholder, the number is divided by a thousand.
            // - Otherwise, it's a string.
            case "THOUSANDS_SEPARATOR":
                if (i - 1 === lastIndexOfDigit) {
                    magnitude += 1;
                    lastIndexOfDigit++; // Can have multiple commas in a row
                    parsedPart.push(token);
                }
                else if (tokens[i + 1]?.type === "DIGIT" && tokens[i - 1]?.type === "DIGIT") {
                    if (parsedPart === integerPart) {
                        hasThousandSeparator = true;
                    }
                    parsedPart.push(token);
                }
                else {
                    parsedPart.push({ type: "CHAR", value: "," });
                }
                break;
        }
    }
    return {
        type: "number",
        integerPart,
        decimalPart,
        percentSymbols,
        thousandsSeparator: hasThousandSeparator,
        magnitude,
    };
}
function parseDateFormatTokens(tokens) {
    const internalFormat = tokens && areValidDateFormatTokens(tokens) ? { type: "date", tokens } : undefined;
    if (!internalFormat) {
        return undefined;
    }
    if (internalFormat.tokens.length &&
        internalFormat.tokens.every((token) => token.type === "DATE_PART" && token.value === "a")) {
        throw new Error("Invalid date format");
    }
    const dateTokens = internalFormat.tokens.map((token) => {
        if (token.type === "THOUSANDS_SEPARATOR" || token.type === "DECIMAL_POINT") {
            return { type: "CHAR", value: token.value };
        }
        return token;
    });
    const convertedTokens = convertTokensToMinutesInDateFormat(dateTokens);
    return { type: "date", tokens: convertedTokens };
}
function tokensToTextInternalFormat(tokens) {
    return tokens && areValidTextFormatTokens(tokens) ? { type: "text", tokens } : undefined;
}
/**
 * Replace in place tokens "mm" and "m" that denote minutes in date format with "MM" to avoid confusion with months.
 *
 * As per OpenXML specification, in date formats if a date token "m" or "mm" is followed by a date token "s" or
 * preceded by a data token "h", then it's not a month but a minute.
 */
function convertTokensToMinutesInDateFormat(tokens) {
    const dateParts = tokens.filter((token) => token.type === "DATE_PART");
    for (let i = 0; i < dateParts.length; i++) {
        if (!dateParts[i].value.startsWith("m") || dateParts[i].value.length > 2) {
            continue;
        }
        if (dateParts[i - 1]?.value.startsWith("h") || dateParts[i + 1]?.value.startsWith("s")) {
            dateParts[i].value = dateParts[i].value.replaceAll("m", "M");
        }
    }
    return tokens;
}
function convertInternalFormatToFormat(internalFormat) {
    return [
        internalFormatPartToFormat(internalFormat.positive),
        internalFormatPartToFormat(internalFormat.negative),
        internalFormatPartToFormat(internalFormat.zero),
        internalFormatPartToFormat(internalFormat.text),
    ]
        .filter(isDefined)
        .join(";");
}
function internalFormatPartToFormat(internalFormat) {
    if (!internalFormat) {
        return undefined;
    }
    let format = "";
    const tokens = internalFormat.type !== "number"
        ? internalFormat.tokens
        : numberInternalFormatToTokenList(internalFormat);
    for (const token of tokens) {
        switch (token.type) {
            case "STRING":
                format += `[$${token.value}]`;
                break;
            case "CHAR":
                format += shouldEscapeFormatChar(token.value) ? `\\${token.value}` : token.value;
                break;
            case "REPEATED_CHAR":
                format += "*" + token.value;
                break;
            case "DATE_PART":
                format += token.value === "MM" ? "mm" : token.value; // Convert "MM" back to "mm" for minutes
                break;
            default:
                format += token.value;
        }
    }
    return format;
}
function numberInternalFormatToTokenList(internalFormat) {
    const tokens = [...internalFormat.integerPart];
    if (internalFormat.decimalPart) {
        tokens.push({ type: "DECIMAL_POINT", value: "." });
        tokens.push(...internalFormat.decimalPart);
    }
    return tokens;
}
function shouldEscapeFormatChar(char) {
    return !alwaysEscapedCharsInFormat.has(char);
}

/**
 * Number of digits for the default number format. This number of digit make a number fit well in a cell
 * with default size and default font size.
 */
const DEFAULT_FORMAT_NUMBER_OF_DIGITS = 11;
const REPEATED_CHAR_PLACEHOLDER = "REPEATED_CHAR_PLACEHOLDER_";
// TODO in the future : remove these constants MONTHS/DAYS, and use a library such as luxon to handle it
// + possibly handle automatic translation of day/month
const MONTHS = {
    0: _t("January"),
    1: _t("February"),
    2: _t("March"),
    3: _t("April"),
    4: _t("May"),
    5: _t("June"),
    6: _t("July"),
    7: _t("August"),
    8: _t("September"),
    9: _t("October"),
    10: _t("November"),
    11: _t("December"),
};
const DAYS$1 = {
    0: _t("Sunday"),
    1: _t("Monday"),
    2: _t("Tuesday"),
    3: _t("Wednesday"),
    4: _t("Thursday"),
    5: _t("Friday"),
    6: _t("Saturday"),
};
function formatOrHumanizeValue(value, format, locale, humanize = false) {
    if (humanize) {
        return humanizeNumber({ value, format }, locale);
    }
    else {
        return formatValue(value, { format, locale });
    }
}
/**
 * Formats a cell value with its format.
 */
function formatValue(value, { format, locale, formatWidth }) {
    if (typeof value === "boolean") {
        value = value ? "TRUE" : "FALSE";
    }
    switch (typeof value) {
        case "string": {
            if (value.includes('\\"')) {
                value = value.replaceAll(/\\"/g, '"');
            }
            if (!format) {
                return value;
            }
            const internalFormat = parseFormat(format);
            const formatToApply = internalFormat.text || internalFormat.positive;
            if (!formatToApply || formatToApply.type !== "text") {
                return value;
            }
            return applyTextInternalFormat(value, formatToApply, formatWidth);
        }
        case "number":
            if (!format) {
                format = createDefaultFormat(value);
            }
            const internalFormat = parseFormat(format);
            if (internalFormat.positive.type === "text") {
                return applyTextInternalFormat(value.toString(), internalFormat.positive, formatWidth);
            }
            let formatToApply = internalFormat.positive;
            if (value < 0 && internalFormat.negative) {
                formatToApply = internalFormat.negative;
                value = -value;
            }
            else if (value === 0 && internalFormat.zero) {
                formatToApply = internalFormat.zero;
            }
            if (formatToApply.type === "date") {
                return repeatCharToFitWidth(applyDateTimeFormat(value, formatToApply), formatWidth);
            }
            const isNegative = value < 0;
            const formatted = repeatCharToFitWidth(applyInternalNumberFormat(Math.abs(value), formatToApply, locale), formatWidth);
            return isNegative ? "-" + formatted : formatted;
        case "object": // case value === null
            return "";
    }
}
function applyTextInternalFormat(value, internalFormat, formatWidth) {
    let formattedValue = "";
    for (const token of internalFormat.tokens) {
        switch (token.type) {
            case "TEXT_PLACEHOLDER":
                formattedValue += value;
                break;
            case "CHAR":
            case "STRING":
                formattedValue += token.value;
                break;
            case "REPEATED_CHAR":
                formattedValue += REPEATED_CHAR_PLACEHOLDER + token.value;
                break;
        }
    }
    return repeatCharToFitWidth(formattedValue, formatWidth);
}
function repeatCharToFitWidth(formattedValue, formatWidth) {
    const placeholderIndex = formattedValue.indexOf(REPEATED_CHAR_PLACEHOLDER);
    if (placeholderIndex === -1) {
        return formattedValue;
    }
    const prefix = formattedValue.slice(0, placeholderIndex);
    const suffix = formattedValue.slice(placeholderIndex + REPEATED_CHAR_PLACEHOLDER.length + 1);
    const repeatedChar = formattedValue[placeholderIndex + REPEATED_CHAR_PLACEHOLDER.length];
    function getTimesToRepeat() {
        if (!formatWidth) {
            return { timesToRepeat: 0, padding: "" };
        }
        const widthTaken = formatWidth.measureText(prefix + suffix);
        const charWidth = formatWidth.measureText(repeatedChar);
        const availableWidth = formatWidth.availableWidth - widthTaken;
        if (availableWidth <= 0) {
            return { timesToRepeat: 0, padding: "" };
        }
        const timesToRepeat = Math.floor(availableWidth / charWidth);
        const remainingWidth = availableWidth - timesToRepeat * charWidth;
        const paddingChar = "\u2009"; // thin space
        const paddingWidth = formatWidth.measureText(paddingChar);
        const padding = paddingChar.repeat(Math.floor(remainingWidth / paddingWidth));
        return { timesToRepeat, padding };
    }
    const { timesToRepeat, padding } = getTimesToRepeat();
    return prefix + repeatedChar.repeat(timesToRepeat) + padding + suffix;
}
function applyInternalNumberFormat(value, format, locale) {
    if (value === Infinity) {
        return "∞" + (format.percentSymbols ? "%" : "");
    }
    const multiplier = format.percentSymbols * 2 - format.magnitude * 3;
    value = value * 10 ** multiplier;
    let maxDecimals = 0;
    if (format.decimalPart !== undefined) {
        maxDecimals = format.decimalPart.filter((token) => token.type === "DIGIT").length;
    }
    const { integerDigits, decimalDigits } = splitNumber(Math.abs(value), maxDecimals);
    let formattedValue = applyIntegerFormat(integerDigits, format, format.thousandsSeparator ? locale.thousandsSeparator : undefined);
    if (format.decimalPart !== undefined) {
        formattedValue += locale.decimalSeparator + applyDecimalFormat(decimalDigits || "", format);
    }
    return formattedValue;
}
function applyIntegerFormat(integerDigits, internalFormat, thousandsSeparator) {
    let tokens = internalFormat.integerPart;
    if (!tokens.some((token) => token.type === "DIGIT")) {
        tokens = [...tokens, { type: "DIGIT", value: "#" }];
    }
    if (integerDigits === "0") {
        integerDigits = "";
    }
    let formattedInteger = "";
    const firstDigitIndex = tokens.findIndex((token) => token.type === "DIGIT");
    let indexInIntegerString = integerDigits.length - 1;
    function appendDigitToFormattedValue(digit, digitType) {
        if (digitType === "0") {
            digit = digit || "0";
        }
        if (!digit)
            return;
        const digitIndex = integerDigits.length - 1 - indexInIntegerString;
        if (thousandsSeparator && digitIndex > 0 && digitIndex % 3 === 0) {
            formattedInteger = digit + thousandsSeparator + formattedInteger;
        }
        else {
            formattedInteger = digit + formattedInteger;
        }
    }
    for (let i = tokens.length - 1; i >= 0; i--) {
        const token = tokens[i];
        switch (token.type) {
            case "DIGIT":
                const digit = integerDigits[indexInIntegerString];
                appendDigitToFormattedValue(digit, token.value);
                indexInIntegerString--;
                // Apply the rest of the integer digits at the first digit character
                if (firstDigitIndex === i) {
                    while (indexInIntegerString >= 0) {
                        appendDigitToFormattedValue(integerDigits[indexInIntegerString], "0");
                        indexInIntegerString--;
                    }
                }
                break;
            case "THOUSANDS_SEPARATOR":
                break;
            case "REPEATED_CHAR":
                formattedInteger = REPEATED_CHAR_PLACEHOLDER + token.value + formattedInteger;
                break;
            default:
                formattedInteger = token.value + formattedInteger;
                break;
        }
    }
    return formattedInteger;
}
function applyDecimalFormat(decimalDigits, internalFormat) {
    if (!internalFormat.decimalPart) {
        return "";
    }
    let formattedDecimals = "";
    let indexInDecimalString = 0;
    for (const token of internalFormat.decimalPart) {
        switch (token.type) {
            case "DIGIT":
                const digit = token.value === "#"
                    ? decimalDigits[indexInDecimalString] || ""
                    : decimalDigits[indexInDecimalString] || "0";
                formattedDecimals += digit;
                indexInDecimalString++;
                break;
            case "THOUSANDS_SEPARATOR":
                break;
            case "REPEATED_CHAR":
                formattedDecimals += REPEATED_CHAR_PLACEHOLDER + token.value;
                break;
            default:
                formattedDecimals += token.value;
                break;
        }
    }
    return formattedDecimals;
}
/**
 * this is a cache that can contains number representation formats
 * from 0 (minimum) to 20 (maximum) digits after the decimal point
 */
const numberRepresentation = [];
/** split a number into two strings that contain respectively:
 * - all digit stored in the integer part of the number
 * - all digit stored in the decimal part of the number
 *
 * The 'maxDecimal' parameter allows to indicate the number of digits to not
 * exceed in the decimal part, in which case digits are rounded.
 *
 **/
function splitNumber(value, maxDecimals = MAX_DECIMAL_PLACES) {
    const asString = value.toString();
    if (asString.includes("e"))
        return splitNumberIntl(value, maxDecimals);
    if (Number.isInteger(value)) {
        return { integerDigits: asString, decimalDigits: undefined };
    }
    const indexOfDot = asString.indexOf(".");
    let integerDigits = asString.substring(0, indexOfDot);
    let decimalDigits = asString.substring(indexOfDot + 1);
    if (maxDecimals === 0) {
        if (Number(decimalDigits[0]) >= 5) {
            integerDigits = (Number(integerDigits) + 1).toString();
        }
        return { integerDigits, decimalDigits: undefined };
    }
    if (decimalDigits.length > maxDecimals) {
        const { integerDigits: roundedIntegerDigits, decimalDigits: roundedDecimalDigits } = limitDecimalDigits(decimalDigits, maxDecimals);
        decimalDigits = roundedDecimalDigits;
        if (roundedIntegerDigits !== "0") {
            integerDigits = (Number(integerDigits) + Number(roundedIntegerDigits)).toString();
        }
    }
    return { integerDigits, decimalDigits: removeTrailingZeroes(decimalDigits || "") };
}
/**
 *  Return the given string minus the trailing "0" characters.
 *
 * @param numberString : a string of integers
 * @returns the numberString, minus the eventual zeroes at the end
 */
function removeTrailingZeroes(numberString) {
    let i = numberString.length - 1;
    while (i >= 0 && numberString[i] === "0") {
        i--;
    }
    return numberString.slice(0, i + 1) || undefined;
}
const leadingZeroesRegexp = /^0+/;
/**
 * Limit the size of the decimal part of a number to the given number of digits.
 */
function limitDecimalDigits(decimalDigits, maxDecimals) {
    let integerDigits = "0";
    let resultDecimalDigits = decimalDigits;
    // Note : we'd want to simply use number.toFixed() to handle the max digits & rounding,
    // but it has very strange behaviour. Ex: 12.345.toFixed(2) => "12.35", but 1.345.toFixed(2) => "1.34"
    const slicedDecimalDigits = decimalDigits.slice(0, maxDecimals);
    const i = maxDecimals;
    if (Number(decimalDigits[i]) < 5) {
        return { integerDigits, decimalDigits: slicedDecimalDigits };
    }
    // round up
    const leadingZeroes = slicedDecimalDigits.match(leadingZeroesRegexp)?.[0] || "";
    const slicedRoundedUp = (Number(slicedDecimalDigits) + 1).toString();
    const withoutLeadingZeroes = slicedDecimalDigits.slice(leadingZeroes.length);
    // e.g. carry over from 99 to 100
    const carryOver = slicedRoundedUp.length > withoutLeadingZeroes.length;
    if (carryOver && !leadingZeroes) {
        integerDigits = "1";
        resultDecimalDigits = undefined;
    }
    else if (carryOver) {
        resultDecimalDigits = leadingZeroes.slice(0, -1) + slicedRoundedUp;
    }
    else {
        resultDecimalDigits = leadingZeroes + slicedRoundedUp;
    }
    return { integerDigits, decimalDigits: resultDecimalDigits };
}
/**
 * Split numbers into decimal/integer digits using Intl.NumberFormat.
 * Supports numbers with a lot of digits that are transformed to scientific notation by
 * number.toString(), but is slow.
 */
function splitNumberIntl(value, maxDecimals = MAX_DECIMAL_PLACES) {
    let formatter = numberRepresentation[maxDecimals];
    if (!formatter) {
        formatter = new Intl.NumberFormat("en-US", {
            maximumFractionDigits: maxDecimals,
            useGrouping: false,
        });
        numberRepresentation[maxDecimals] = formatter;
    }
    const [integerDigits, decimalDigits] = formatter.format(value).split(".");
    return { integerDigits, decimalDigits };
}
/** Convert a number into a string, without scientific notation */
function numberToString(number, decimalSeparator) {
    const { integerDigits, decimalDigits } = splitNumber(number, 20);
    return decimalDigits ? integerDigits + decimalSeparator + decimalDigits : integerDigits;
}
/**
 * Check if the given format is a time, date or date time format. Only check the first part of a multi-part format.
 */
const isDateTimeFormat = memoize(function isDateTimeFormat(format) {
    if (!format) {
        return false;
    }
    try {
        const internalFormat = parseFormat(format);
        return internalFormat.positive.type === "date";
    }
    catch (error) {
        return false;
    }
});
function applyDateTimeFormat(value, internalFormat) {
    const jsDate = numberToJsDate(value);
    const isMeridian = internalFormat.tokens.some((token) => token.type === "DATE_PART" && token.value === "a");
    let currentValue = "";
    for (const token of internalFormat.tokens) {
        switch (token.type) {
            case "DATE_PART":
                currentValue += formatJSDatePart(jsDate, token.value, isMeridian);
                break;
            case "REPEATED_CHAR":
                currentValue += REPEATED_CHAR_PLACEHOLDER + token.value;
                break;
            default:
                currentValue += token.value;
                break;
        }
    }
    return currentValue;
}
function formatJSDatePart(jsDate, tokenValue, isMeridian) {
    switch (tokenValue) {
        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":
            // force translation because somehow node 22 doesn't call LazyTranslatedString.toString() whe concatenating it to a string
            return DAYS$1[jsDate.getDay()].toString();
        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()].toString();
        case "mmmmm":
            return MONTHS[jsDate.getMonth()].slice(0, 1);
        case "qq":
            return _t("Q%(quarter)s", { quarter: jsDate.getQuarter() }).toString();
        case "qqqq":
            return _t("Quarter %(quarter)s", { quarter: jsDate.getQuarter() }).toString();
        case "yy":
            const fullYear = String(jsDate.getFullYear()).replace("-", "").padStart(2, "0");
            return fullYear.slice(fullYear.length - 2);
        case "yyyy":
            return jsDate.getFullYear();
        case "hhhh":
            const elapsedHours = Math.floor((jsDate.getTime() - INITIAL_1900_DAY.getTime()) / (60 * 60 * 1000));
            return elapsedHours.toString();
        case "hh":
            const dateHours = jsDate.getHours();
            let hours = dateHours;
            if (isMeridian) {
                hours = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
            }
            return hours.toString().padStart(2, "0");
        case "MM": // "MM" replaces "mm" for minutes during format parsing
            return jsDate.getMinutes().toString().padStart(2, "0");
        case "ss":
            return jsDate.getSeconds().toString().padStart(2, "0");
        case "a":
            return jsDate.getHours() >= 12 ? "PM" : "AM";
        default:
            throw new Error(`invalid date format token: ${tokenValue}`);
    }
}
/**
 * Get a regex matching decimal number based on the locale's thousand separator
 *
 * eg. if the locale's thousand separator is a comma, this will return a regex /[0-9]+,[0-9]/
 */
const getDecimalNumberRegex = memoize(function getDecimalNumberRegex(locale) {
    return new RegExp(`[0-9]+${escapeRegExp(locale.decimalSeparator)}[0-9]`);
});
// -----------------------------------------------------------------------------
// CREATE / MODIFY FORMAT
// -----------------------------------------------------------------------------
/**
 * Create a default format for a number.
 *
 * If possible this will try round the number to have less than DEFAULT_FORMAT_NUMBER_OF_DIGITS characters
 * in the number. This is obviously only possible for number with a big decimal part. For number with a lot
 * of digits in the integer part, keep the number as it is.
 */
function createDefaultFormat(value) {
    let { integerDigits, decimalDigits } = splitNumber(value);
    if (!decimalDigits)
        return "0";
    const digitsInIntegerPart = integerDigits.replace("-", "").length;
    // If there's no space for at least the decimal separator + a decimal digit, don't display decimals
    if (digitsInIntegerPart + 2 > DEFAULT_FORMAT_NUMBER_OF_DIGITS) {
        return "0";
    }
    // -1 for the decimal separator character
    const spaceForDecimalsDigits = DEFAULT_FORMAT_NUMBER_OF_DIGITS - digitsInIntegerPart - 1;
    ({ decimalDigits } = splitNumber(value, Math.min(spaceForDecimalsDigits, decimalDigits.length)));
    return decimalDigits ? "0." + "0".repeat(decimalDigits.length) : "0";
}
function detectDateFormat(content, locale) {
    if (!isDateTime(content, locale)) {
        return undefined;
    }
    const internalDate = parseDateTime(content, locale);
    return internalDate.format;
}
/** use this function only if the content corresponds to a number (means that isNumber(content) return true */
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;
}
function createCurrencyFormat(currency) {
    const decimalPlaces = currency.decimalPlaces ?? 2;
    const position = currency.position ?? "before";
    const code = currency.code ?? "";
    const symbol = currency.symbol ?? "";
    const decimalRepresentation = decimalPlaces ? "." + "0".repeat(decimalPlaces) : "";
    const numberFormat = "#,##0" + decimalRepresentation;
    let textExpression = `${code} ${symbol}`.trim();
    if (position === "after" && code) {
        textExpression = " " + textExpression;
    }
    return insertTextInFormat(textExpression, position, numberFormat);
}
function createAccountingFormat(currency) {
    const decimalPlaces = currency.decimalPlaces ?? 2;
    const position = currency.position ?? "before";
    const code = currency.code ?? "";
    const symbol = currency.symbol ?? "";
    const decimalRepresentation = decimalPlaces ? "." + "0".repeat(decimalPlaces) : "";
    const numberFormat = "#,##0" + decimalRepresentation;
    let textExpression = `${code} ${symbol}`.trim();
    if (position === "after" && code) {
        textExpression = " " + textExpression;
    }
    const positivePart = insertTextInAccountingFormat(textExpression, position, ` ${numberFormat} `);
    const negativePart = insertTextInAccountingFormat(textExpression, position, `(${numberFormat})`);
    const zeroPart = insertTextInAccountingFormat(textExpression, position, "  -  ");
    return [positivePart, negativePart, zeroPart].join(";");
}
function insertTextInAccountingFormat(text, position, format) {
    const textExpression = `[$${text}]`;
    return position === "before" ? textExpression + "* " + format : format + "* " + textExpression;
}
function insertTextInFormat(text, position, format) {
    const textExpression = `[$${text}]`;
    return position === "before" ? textExpression + format : format + textExpression;
}
function roundFormat(format) {
    const multiPartFormat = parseFormat(format);
    const roundedInternalFormat = {
        positive: _roundFormat(multiPartFormat.positive),
        negative: multiPartFormat.negative ? _roundFormat(multiPartFormat.negative) : undefined,
        zero: multiPartFormat.zero ? _roundFormat(multiPartFormat.zero) : undefined,
        text: multiPartFormat.text,
    };
    return convertInternalFormatToFormat(roundedInternalFormat);
}
function _roundFormat(internalFormat) {
    if (internalFormat.type !== "number" || !internalFormat.decimalPart) {
        return internalFormat;
    }
    const nonDigitDecimalPart = internalFormat.decimalPart.filter((token) => token.type !== "DIGIT");
    return {
        ...internalFormat,
        decimalPart: undefined,
        integerPart: [...internalFormat.integerPart, ...nonDigitDecimalPart],
    };
}
function humanizeNumber({ value, format }, locale) {
    const numberValue = tryToNumber(value, locale);
    if (numberValue === undefined) {
        return "";
    }
    let numberFormat = format;
    if (Math.abs(numberValue) < 1000) {
        const hasDecimal = numberValue % 1 !== 0;
        numberFormat = !format && hasDecimal ? "0.####" : format;
    }
    else {
        numberFormat = formatLargeNumber({ value, format }, undefined, locale);
    }
    return formatValue(value, { format: numberFormat, locale });
}
function formatLargeNumber(arg, unit, locale) {
    let value = 0;
    try {
        value = Math.abs(toNumber(arg?.value, locale));
    }
    catch (e) {
        return "";
    }
    const format = arg?.format;
    if (unit !== undefined) {
        const postFix = unit?.value;
        switch (postFix) {
            case "k":
                return createLargeNumberFormat(format, 1, "k");
            case "m":
                return createLargeNumberFormat(format, 2, "m");
            case "b":
                return createLargeNumberFormat(format, 3, "b");
            default:
                throw new EvaluationError(_t("The formatting unit should be 'k', 'm' or 'b'."));
        }
    }
    if (value < 1e5) {
        return createLargeNumberFormat(format, 0, "");
    }
    else if (value < 1e8) {
        return createLargeNumberFormat(format, 1, "k");
    }
    else if (value < 1e11) {
        return createLargeNumberFormat(format, 2, "m");
    }
    return createLargeNumberFormat(format, 3, "b");
}
function createLargeNumberFormat(format, magnitude, postFix, locale) {
    const multiPartFormat = parseFormat(format || "#,##0");
    const roundedInternalFormat = {
        positive: _createLargeNumberFormat(multiPartFormat.positive, magnitude, postFix),
        negative: multiPartFormat.negative
            ? _createLargeNumberFormat(multiPartFormat.negative, magnitude, postFix)
            : undefined,
        zero: multiPartFormat.zero
            ? _createLargeNumberFormat(multiPartFormat.zero, magnitude, postFix)
            : undefined,
        text: multiPartFormat.text,
    };
    return convertInternalFormatToFormat(roundedInternalFormat);
}
function _createLargeNumberFormat(format, magnitude, postFix) {
    if (format.type !== "number") {
        return format;
    }
    const postFixToken = { type: "STRING", value: postFix };
    let newIntegerPart = [...format.integerPart];
    const lastDigitIndex = newIntegerPart.findLastIndex((token) => token.type === "DIGIT");
    if (lastDigitIndex === -1) {
        throw new Error("Cannot create a large number format from a format with no digit.");
    }
    while (newIntegerPart[lastDigitIndex + 1]?.type === "THOUSANDS_SEPARATOR") {
        newIntegerPart = removeIndexesFromArray(newIntegerPart, [lastDigitIndex + 1]);
    }
    const tokenAfterDigits = newIntegerPart[lastDigitIndex + 1];
    if (tokenAfterDigits?.type === "STRING" && ["m", "k", "b"].includes(tokenAfterDigits.value)) {
        newIntegerPart = replaceItemAtIndex(newIntegerPart, postFixToken, lastDigitIndex + 1);
    }
    else {
        newIntegerPart = insertItemsAtIndex(newIntegerPart, [postFixToken], lastDigitIndex + 1);
    }
    if (magnitude > 0) {
        newIntegerPart = insertItemsAtIndex(newIntegerPart, Array(magnitude).fill({ type: "THOUSANDS_SEPARATOR", value: "," }), lastDigitIndex + 1);
    }
    const missingPercents = format.percentSymbols - newIntegerPart.filter((tk) => tk.type === "PERCENT").length;
    newIntegerPart.push(...new Array(missingPercents).fill({ type: "PERCENT", value: "%" }));
    return { ...format, integerPart: newIntegerPart, decimalPart: undefined, magnitude };
}
function changeDecimalPlaces(format, step) {
    const multiPartFormat = parseFormat(format);
    const newInternalFormat = {
        positive: _changeDecimalPlace(multiPartFormat.positive, step),
        negative: multiPartFormat.negative
            ? _changeDecimalPlace(multiPartFormat.negative, step)
            : undefined,
        zero: multiPartFormat.zero ? _changeDecimalPlace(multiPartFormat.zero, step) : undefined,
        text: multiPartFormat.text,
    };
    // Re-parse the format to make sure we don't break the number of digit limit
    return convertInternalFormatToFormat(parseFormat(convertInternalFormatToFormat(newInternalFormat)));
}
function _changeDecimalPlace(format, step) {
    if (format.type !== "number") {
        return format;
    }
    return (step > 0 ? addDecimalPlaces(format, step) : removeDecimalPlaces(format, Math.abs(step)));
}
function removeDecimalPlaces(format, step) {
    let decimalPart = format.decimalPart;
    if (!decimalPart) {
        return format;
    }
    const indexesToRemove = [];
    let digitCount = 0;
    for (let i = decimalPart.length - 1; i >= 0; i--) {
        if (digitCount >= Math.abs(step)) {
            break;
        }
        if (decimalPart[i].type === "DIGIT") {
            digitCount++;
            indexesToRemove.push(i);
        }
    }
    decimalPart = removeIndexesFromArray(decimalPart, indexesToRemove);
    if (decimalPart.some((token) => token.type === "DIGIT")) {
        return { ...format, decimalPart };
    }
    return {
        ...format,
        decimalPart: undefined,
        integerPart: [...format.integerPart, ...decimalPart],
    };
}
function addDecimalPlaces(format, step) {
    let integerPart = format.integerPart;
    let decimalPart = format.decimalPart;
    if (!decimalPart) {
        const lastDigitIndex = integerPart.findLastIndex((token) => token.type === "DIGIT");
        decimalPart = integerPart.slice(lastDigitIndex + 1);
        integerPart = integerPart.slice(0, lastDigitIndex + 1);
    }
    const digitsToAdd = range(0, step).map(() => ({ type: "DIGIT", value: "0" }));
    const lastDigitIndex = decimalPart.findLastIndex((token) => token.type === "DIGIT");
    if (lastDigitIndex === -1) {
        decimalPart = [...digitsToAdd, ...decimalPart];
    }
    else {
        decimalPart = insertItemsAtIndex(decimalPart, digitsToAdd, lastDigitIndex + 1);
    }
    return { ...format, decimalPart, integerPart };
}
function isExcelCompatible(format) {
    const internalFormat = parseFormat(format);
    for (const part of [internalFormat.positive, internalFormat.negative, internalFormat.zero]) {
        if (part &&
            part.type === "date" &&
            part.tokens.some((token) => token.type === "DATE_PART" && token.value.includes("q"))) {
            return false;
        }
    }
    return true;
}
function isTextFormat(format) {
    if (!format)
        return false;
    try {
        const internalFormat = parseFormat(format);
        return internalFormat.positive.type === "text";
    }
    catch {
        return false;
    }
}

function evaluateLiteral(literalCell, localeFormat, position) {
    const value = isTextFormat(localeFormat.format) && literalCell.parsedValue !== null
        ? literalCell.content
        : literalCell.parsedValue;
    const functionResult = { value, format: localeFormat.format, origin: position };
    return createEvaluatedCell(functionResult, localeFormat.locale);
}
function parseLiteral(content, locale) {
    if (content.startsWith("=")) {
        throw new Error(`Cannot parse "${content}" because it's not a literal value. It's a formula`);
    }
    if (content === "") {
        return null;
    }
    if (content.includes("\n")) {
        return content;
    }
    if (isNumber(content, DEFAULT_LOCALE)) {
        return parseNumber(content, DEFAULT_LOCALE);
    }
    const internalDate = parseDateTime(content, locale);
    if (internalDate) {
        return internalDate.value;
    }
    if (isBoolean(content)) {
        return content.toUpperCase() === "TRUE";
    }
    return content;
}
function createEvaluatedCell(functionResult, locale = DEFAULT_LOCALE, cell, origin) {
    const link = detectLink(functionResult.value);
    if (!link) {
        const evaluateCell = _createEvaluatedCell(functionResult, locale, cell);
        return addOrigin(evaluateCell, functionResult.origin ?? origin);
    }
    const value = parseLiteral(link.label, locale);
    const format = functionResult.format ||
        (typeof value === "number"
            ? detectDateFormat(link.label, locale) || detectNumberFormat(link.label)
            : undefined);
    const linkPayload = {
        value,
        format,
    };
    return addOrigin({
        ..._createEvaluatedCell(linkPayload, locale, cell),
        link,
    }, functionResult.origin ?? origin);
}
function _createEvaluatedCell(functionResult, locale, cell) {
    let { value, format, message } = functionResult;
    format = cell?.format || format;
    const formattedValue = formatValue(value, { format, locale });
    if (isEvaluationError(value)) {
        return errorCell(value, message);
    }
    if (value === null) {
        return emptyCell(format);
    }
    if (isTextFormat(format)) {
        // TO DO:
        // with the next line, the value of the cell is transformed depending on the format.
        // This shouldn't happen, by doing this, the formulas handling numbers are not able
        // to interpret the value as a number.
        return textCell(toString(value), format, formattedValue);
    }
    if (typeof value === "number") {
        if (isDateTimeFormat(format || "")) {
            return dateTimeCell(value, format, formattedValue);
        }
        return numberCell(value, format, formattedValue);
    }
    if (typeof value === "boolean") {
        return booleanCell(value, format, formattedValue);
    }
    return textCell(value, format, formattedValue);
}
function textCell(value, format, formattedValue) {
    return {
        value,
        format,
        formattedValue,
        type: CellValueType.text,
        isAutoSummable: true,
        defaultAlign: "left",
    };
}
function numberCell(value, format, formattedValue) {
    return {
        value: value || 0, // necessary to avoid "-0" and NaN values,
        format,
        formattedValue,
        type: CellValueType.number,
        isAutoSummable: true,
        defaultAlign: "right",
    };
}
const emptyCell = memoize(function emptyCell(format) {
    return {
        value: null,
        format,
        formattedValue: "",
        type: CellValueType.empty,
        isAutoSummable: true,
        defaultAlign: "left",
    };
});
function dateTimeCell(value, format, formattedValue) {
    return {
        value,
        format,
        formattedValue,
        type: CellValueType.number,
        isAutoSummable: false,
        defaultAlign: "right",
    };
}
function booleanCell(value, format, formattedValue) {
    return {
        value,
        format,
        formattedValue,
        type: CellValueType.boolean,
        isAutoSummable: false,
        defaultAlign: "center",
    };
}
function errorCell(value, message) {
    return {
        value,
        formattedValue: value,
        message,
        type: CellValueType.error,
        isAutoSummable: false,
        defaultAlign: "center",
    };
}
function addOrigin(cell, origin) {
    if (cell.value === null) {
        // ignore empty cells to allow sharing the same object instance
        return cell;
    }
    if ("origin" in cell) {
        return cell;
    }
    cell.origin = origin;
    return cell;
}

function toCriterionDateNumber(dateValue) {
    const today = DateTime.now();
    switch (dateValue) {
        case "today":
            return Math.floor(jsDateToNumber(today));
        case "yesterday": {
            today.setDate(today.getDate() - 1);
            return Math.floor(jsDateToNumber(today));
        }
        case "tomorrow": {
            today.setDate(today.getDate() + 1);
            return Math.floor(jsDateToNumber(today));
        }
        case "lastWeek":
            today.setDate(today.getDate() - 6);
            return Math.floor(jsDateToNumber(today));
        case "lastMonth": {
            const lastMonth = today.getMonth() === 0 ? 11 : today.getMonth() - 1;
            const dateInLastMonth = new DateTime(today.getFullYear(), lastMonth, 1);
            if (today.getDate() > getDaysInMonth(dateInLastMonth)) {
                today.setDate(1);
            }
            else {
                today.setDate(today.getDate() + 1);
                today.setMonth(today.getMonth() - 1);
            }
            return Math.floor(jsDateToNumber(today));
        }
        case "lastYear":
            // Handle leap year case
            if (today.getMonth() === 1 && today.getDate() === 29) {
                today.setDate(28);
                today.setFullYear(today.getFullYear() - 1);
            }
            else {
                today.setDate(today.getDate() + 1);
                today.setFullYear(today.getFullYear() - 1);
            }
            return Math.floor(jsDateToNumber(today));
    }
}
/** Get all the dates values of a criterion converted to numbers, converting date values such as "today" to actual dates  */
function getDateNumberCriterionValues(criterion, locale) {
    if ("dateValue" in criterion && criterion.dateValue !== "exactDate") {
        return [toCriterionDateNumber(criterion.dateValue)];
    }
    return criterion.values.map((value) => valueToDateNumber(value, locale));
}
function getDateCriterionFormattedValues(values, locale) {
    return values.map((valueStr) => {
        if (valueStr.startsWith("=")) {
            return valueStr;
        }
        const value = parseLiteral(valueStr, locale);
        if (typeof value === "number") {
            return formatValue(value, { format: locale.dateFormat, locale });
        }
        return "";
    });
}

/**
 * ####################################################
 * # INTRODUCTION
 * ####################################################
 *
 * This file contain the function recomputeZones.
 * This function try to recompute in a performant way
 * an ensemble of zones possibly overlapping to avoid
 * overlapping and to reduce the number of zones.
 *
 * It also allows to remove some zones from the ensemble.
 *
 * In the following example, 2 zones are overlapping.
 * Applying recomputeZones will return zones without
 * overlapping:
 *
 * ["B3:D4", "D2:E3"]         ["B3:C4", "D2:D4", "E2:E3"]
 *
 *      A B C D E                    A B C D E
 *    1       ___                  1       ___
 *    2   ___|_  |                 2   ___| | |
 *    3  |   |_|_|      --->       3  |   | |_|
 *    4  |_____|                   4  |___|_|
 *    6                            6
 *    7                            7
 *
 *
 * In the following example, 2 zones are contiguous.
 * Applying recomputeZones will return only one zone:
 *
 *  ["B2:B3", "C2:D3"]               ["B2:D3"]
 *
 *       A B C D E                   A B C D E
 *     1   _ ___                   1   _____
 *     2  | |   |        --->      2  |     |
 *     3  |_|___|                  3  |_____|
 *     4                           4
 *
 *
 * In the following example, we want to remove a zone
 * from the ensemble. Applying recomputeZones will
 * return the ensemble without the zone to remove:
 *
 *    remove ["C3:D3"]           ["B2:B4", "C2:D2",
 *                                "C4:D4", "E2:E4"]
 *
 *       A B C D E F                 A B C D E F
 *     1   _______                 1   _______
 *     2  |       |       --->     2  | |___| |
 *     3  |  xxx  |                3  | |___| |
 *     4  |_______|                4  |_|___|_|
 *     5                           5
 *
 *
 * The exercise seems simple when we have only 2 zones.
 * But with n zones and in a performant way, we want to
 * avoid comparing each zone with all the others.
 *
 *
 * ####################################################
 * # Methodological approach
 * ####################################################
 *
 * The methodological approach to avoid comparing each
 * zone with all the others is to use a data structure
 * that allow to quickly find which zones are
 * overlapping with any other given zone.
 *
 * Here the idea is to profile the zones at the columns level.
 *
 * To do that, we propose to use a data structure
 * composed of 2 parts:
 * - profilesStartingPosition: a sorted number array
 * indicating on which columns a new profile begins.
 * - profiles: a map where the key is a column
 * position (from profilesStartingPosition) and the
 * value is a sorted number array representing a
 * profile.
 *
 *
 * See the following example:    here profileStartingPosition
 *                               corresponds to [A,C,E,G,K]
 *    A B C D E F G H I J K      so with number [0,2,4,6,10]
 *  1    '   '   '       '
 *  2    '   '   '_______'       here profile correspond
 *  3    '___'   |_______|       for A to []
 *  4    |   |                   for C to [3, 5]
 *  5    |___|                   for E to []
 *  6                            for G to [2, 3]
 *  7                            for K to []
 *
 *
 * Now we can easily find which zones are overlapping
 * with a given zone. Suppose we want to add a new zone
 * D5:H6 to the ensemble:
 *
 *                              With a binary search of left and right
 *    A B C D E F G H I J K     on profilesStartingPosition, we can
 *  1    '   '   '       '      find the indexes of the profiles on which
 *  2    '   '   '_______'      to apply a modification.
 *  3    '___'   |_______|
 *  4    |  _|_______           Here we will:
 *  5    |_|_|       |          - add a new profile in D   --> become [3, 6]
 *  6      |_________|          - modify the profile in E  --> become [4, 6]
 *  7                           - modify the profile in G  --> become [2, 3, 4, 6]
 *                              - add a new profile in I   --> become [8, 10]
 *
 *  See below the result:
 *
 *                              Note the particularity of the profile
 *    A B C D E F G H I J K     for G: it will correspond to [2, 3, 4, 6]
 *  1    ' ' '   '   '   '
 *  2    ' ' '   '___'___'      To know how to modify the profile (add a
 *  3    '_'_'   |___|___|      zone or remove it) we do a binary
 *  4    | | |___ ___           search of the top and bottom value on the
 *  5    |_| |   |   |          profile array. Depending on the result index
 *  6      |_|___|___|          parity (odd or even), because zone boundaries
 *  7                           go by pairs, we know if we are in a zone or
 *                              not and how operate.
 */
/**
 * Recompute the zone without the cells in toRemoveZones and avoid overlapping.
 * This compute is particularly useful because after this function:
 * - you will find coordinate of a cell only once among all the zones
 * - the number of zones will be reduced to the minimum
 */
function recomputeZones(zones, zonesToRemove = []) {
    if (zones.length <= 1 && zonesToRemove.length === 0) {
        return zones;
    }
    const profilesStartingPosition = [0];
    const profiles = new Map([[0, []]]);
    modifyProfiles(profilesStartingPosition, profiles, zones, false);
    modifyProfiles(profilesStartingPosition, profiles, zonesToRemove, true);
    return constructZonesFromProfiles(profilesStartingPosition, profiles);
}
function modifyProfiles(// export for testing only
profilesStartingPosition, profiles, zones, toRemove = false) {
    for (const zone of zones) {
        const leftValue = zone.left;
        const rightValue = zone.right === undefined ? undefined : zone.right + 1;
        const leftIndex = findIndexAndCreateProfile(profilesStartingPosition, profiles, leftValue, true, 0);
        const rightIndex = findIndexAndCreateProfile(profilesStartingPosition, profiles, rightValue, false, leftIndex);
        for (let i = leftIndex; i <= rightIndex; i++) {
            const profile = profiles.get(profilesStartingPosition[i]);
            modifyProfile(profile, zone, toRemove);
        }
        // maybe this part cost in performance, and maybe it's not necessary (depending on the use case). To be checked
        removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex);
    }
}
function profilesContainsZone(profilesStartingPosition, profiles, zone) {
    const leftValue = zone.left;
    const rightValue = zone.right;
    const leftIndex = binaryPredecessorSearch(profilesStartingPosition, leftValue, 0);
    const rightIndex = rightValue === undefined
        ? profilesStartingPosition.length - 1
        : binaryPredecessorSearch(profilesStartingPosition, rightValue, leftIndex);
    /**
     * The `profilesStartingPosition` array always contains at least the value `0` at its first position,
     * ensuring that applying `binaryPredecessorSearch` will always return a valid index.
     * Therefore, it is not necessary to check if the result of `binaryPredecessorSearch` equals `-1`.
     */
    const topValue = zone.top;
    const bottomValue = zone.bottom === undefined ? undefined : zone.bottom + 1;
    for (let i = leftIndex; i <= rightIndex; i++) {
        const profile = profiles.get(profilesStartingPosition[i]);
        const topPredIndex = binaryPredecessorSearch(profile, topValue, 0);
        if (topPredIndex === -1 || topPredIndex % 2 !== 0) {
            return false;
        }
        const bottomSuccIndex = bottomValue === undefined ? profile.length : binarySuccessorSearch(profile, bottomValue, 0);
        if (topPredIndex + 1 !== bottomSuccIndex) {
            return false;
        }
    }
    return true;
}
function findIndexAndCreateProfile(profilesStartingPosition, profiles, value, searchLeft, startIndex) {
    if (value === undefined) {
        // this is only the case when the value correspond to a bottom value that could be undefined
        return profilesStartingPosition.length - 1;
    }
    const predecessorIndex = binaryPredecessorSearch(profilesStartingPosition, value, startIndex);
    if (value !== profilesStartingPosition[predecessorIndex]) {
        // mean that the value is not ending/starting at the same position as the previous/next profile
        // --> it's a new profile
        // --> we need to add it
        profilesStartingPosition.splice(predecessorIndex + 1, 0, value);
        // suppose the               we want to add the       for the left value
        // following profile         following zone:          'C', the predecessor index
        //   for B: [1, 3]                "C3:D4"             correspond to 'B'.
        //                                                    The next line code will
        //       A B C D                A B C D               copy the profile of 'B'
        //     1  '___'               1  '___'                to 'C'. In the rest of the
        //     2  |   |       --->    2  |  _|_               process the 'modifyProfile'
        //     3  |___|               3  |_|_| |              function will adapt the waiting
        //     4                      4    |___|              'C' profile [1, 3] to the
        //                                                    correct 'C' profile [1, 4]
        profiles.set(value, [...profiles.get(profilesStartingPosition[predecessorIndex])]);
        return searchLeft ? predecessorIndex + 1 : predecessorIndex;
    }
    return searchLeft ? predecessorIndex : predecessorIndex - 1;
}
/**
 *  Suppose the following        Suppose we want to add          We want to have the
 *  profile:                     the following zone:             following profile:
 *
 *       A B C D E F                  A B C D E F                     A B C D E F
 *     1    '___'                   1    '   '                      1    '___'
 *     2    |___|                   2    '___'                      2    |   |
 *     3    '   '                   3    |   |                      3    |   |
 *     4    '___'          -->      4    |   |            -->       4    |   |
 *     6    |   |                   6    |___|                      6    |   |
 *     7    |___|                   7                               7    |___|
 *     8                            8                               8
 *
 *  the profile for 'C'          the top zone correspond        Here [2, 3, 5, 8] with [3, 7]
 *  corresponds to:              to 3 and the bottom zone       would be merged into [2, 8]
 *   ____  ____                  correspond to 6
 *  [2, 3, 5, 8]                 would be the profile:          The difficulty of modify profile
 *                                ____                          is to know what must be deleted
 *  Note that the 'filled        [3, 7]                         and what must be added to the
 *  zone' are always between                                    existing profile.
 *  an even index and its
 *  next index
 *
 */
function modifyProfile(profile, zone, toRemove = false) {
    const topValue = zone.top;
    const bottomValue = zone.bottom === undefined ? undefined : zone.bottom + 1;
    const newPoints = [];
    // Case we want to add a zone to the profile:
    // - If the top predecessor index `topPredIndex` is even, it means the top of the zone is already positioned on a filled zone
    // so we don't need to add it to the profile. we can keep in reference the index of the predecessor.
    // - If it is odd, it means the top of the zone must be the beginning of a filled zone.
    // so we can keep the index of the top position
    // Case we want to remove a zone from the profile: it's the opposite of the previous case
    const topPredIndex = binaryPredecessorSearch(profile, topValue, 0, false);
    if ((topPredIndex % 2 !== 0 && !toRemove) || (topPredIndex % 2 === 0 && toRemove)) {
        newPoints.push(topValue);
    }
    if (bottomValue === undefined) {
        // The following two code lines will not impact the final result,
        // but they will impact the intermediate profile.
        // We keep them for performance reason
        profile.splice(topPredIndex + 1);
        profile.push(...newPoints);
        return;
    }
    // Case we want to add a zone to the profile:
    // - If the bottom successor index `bottomSuccIndex` is even, it means the bottom of the zone must be the ending of a filled zone
    // so we can keep the index of the bottom position.
    // - If it is odd, it means the bottom of the zone is already positioned on a filled zone
    // so we don't need to add it to the profile. we can keep in reference the index of the successor
    // Case we want to remove a zone from the profile: it's the opposite of the previous case
    const bottomSuccIndex = binarySuccessorSearch(profile, bottomValue, 0, false);
    if ((bottomSuccIndex % 2 === 0 && !toRemove) || (bottomSuccIndex % 2 !== 0 && toRemove)) {
        newPoints.push(bottomValue);
    }
    // add the top and bottom value to the profile and
    // remove all information between the top and bottom index
    const toDelete = bottomSuccIndex - topPredIndex - 1;
    const toInsert = newPoints.length;
    const start = topPredIndex + 1;
    // fast path and slow path
    if (start === profile.length - 1 && toDelete === 1 && toInsert === 1) {
        // fast path: we just need to replace the last element
        profile[start] = newPoints[0] ?? newPoints[1];
    }
    else {
        // equivalent but slower and with memory allocation
        profile.splice(start, toDelete, ...newPoints);
    }
}
function removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex) {
    const start = leftIndex - 1 === -1 ? 0 : leftIndex - 1;
    const end = rightIndex === profilesStartingPosition.length - 1 ? rightIndex : rightIndex + 1;
    for (let i = end; i > start; i--) {
        if (deepEqualsArray(profiles.get(profilesStartingPosition[i]), profiles.get(profilesStartingPosition[i - 1]))) {
            profiles.delete(profilesStartingPosition[i]);
            profilesStartingPosition.splice(i, 1);
        }
    }
}
function constructZonesFromProfiles(profilesStartingPosition, profiles) {
    const mergedZone = [];
    let pendingZones = [];
    for (let colIndex = 0; colIndex < profilesStartingPosition.length; colIndex++) {
        const left = profilesStartingPosition[colIndex];
        const profile = profiles.get(left);
        if (!profile || profile.length === 0) {
            mergedZone.push(...pendingZones);
            pendingZones = [];
            continue;
        }
        let right = profilesStartingPosition[colIndex + 1];
        if (right !== undefined) {
            right--;
        }
        const nextPendingZones = [];
        for (let i = 0; i < profile.length; i += 2) {
            const top = profile[i];
            let bottom = profile[i + 1];
            if (bottom !== undefined) {
                bottom--;
            }
            const profileZone = {
                top,
                left,
                bottom,
                right,
            };
            if ((bottom === undefined && top !== 0) || (right === undefined && left !== 0)) {
                profileZone.hasHeader = true;
            }
            let findCorrespondingZone = false;
            for (let j = pendingZones.length - 1; j >= 0; j--) {
                const pendingZone = pendingZones[j];
                if (pendingZone.top === profileZone.top && pendingZone.bottom === profileZone.bottom) {
                    pendingZone.right = profileZone.right;
                    pendingZones.splice(j, 1);
                    nextPendingZones.push(pendingZone);
                    findCorrespondingZone = true;
                    break;
                }
            }
            if (!findCorrespondingZone) {
                nextPendingZones.push(profileZone);
            }
        }
        mergedZone.push(...pendingZones);
        pendingZones = nextPendingZones;
    }
    mergedZone.push(...pendingZones);
    return mergedZone;
}
function binaryPredecessorSearch(arr, val, start = 0, matchEqual = true) {
    let end = arr.length - 1;
    let result = -1;
    while (start <= end) {
        const mid = start + ((end - start) >> 1); // same as (start + end) // 2 but faster and without risk of overflow
        if (arr[mid] === val && matchEqual) {
            return mid;
        }
        else if (arr[mid] < val) {
            result = mid;
            start = mid + 1;
        }
        else {
            end = mid - 1;
        }
    }
    return result;
}
function binarySuccessorSearch(arr, val, start = 0, matchEqual = true) {
    let end = arr.length - 1;
    let result = arr.length;
    while (start <= end) {
        const mid = start + ((end - start) >> 1); // same as (start + end) // 2 but faster and without risk of overflow
        if (arr[mid] === val && matchEqual) {
            return mid;
        }
        else if (arr[mid] > val) {
            result = mid;
            end = mid - 1;
        }
        else {
            start = mid + 1;
        }
    }
    return result;
}

/**
 * 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) {
    const chars = new TokenizingChars(xc);
    consumeSpaces(chars);
    const sheetSeparatorIndex = xc.indexOf("!");
    if (sheetSeparatorIndex !== -1) {
        chars.advanceBy(sheetSeparatorIndex + 1);
    }
    const leftLetters = consumeLetters(chars);
    const leftNumbers = consumeDigits(chars);
    let top, bottom, left, right;
    let fullCol = false;
    let fullRow = false;
    let hasHeader = false;
    if (leftNumbers === -1) {
        left = right = leftLetters - 1;
        top = bottom = 0;
        fullCol = true;
    }
    else if (leftLetters === -1) {
        top = bottom = leftNumbers - 1;
        left = right = 0;
        fullRow = true;
    }
    else {
        left = right = leftLetters - 1;
        top = bottom = leftNumbers - 1;
        hasHeader = true;
    }
    consumeSpaces(chars);
    if (chars.current === ":") {
        chars.advanceBy(1);
        consumeSpaces(chars);
        const rightLetters = consumeLetters(chars);
        const rightNumbers = consumeDigits(chars);
        if (rightNumbers === -1) {
            right = rightLetters - 1;
            fullCol = true;
        }
        else if (rightLetters === -1) {
            bottom = rightNumbers - 1;
            fullRow = true;
        }
        else {
            right = rightLetters - 1;
            bottom = rightNumbers - 1;
            top = fullCol ? bottom : top;
            left = fullRow ? right : left;
            hasHeader = true;
        }
    }
    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);
    const orderedZone = reorderZone(zone);
    const bottom = orderedZone.bottom;
    const right = orderedZone.right;
    if ((bottom !== undefined && bottom > MAX_ROW) || (right !== undefined && right > MAX_COL)) {
        throw new Error(`Range string out of bounds: ${xc}`); // limit the size of the zone for performance
    }
    if (bottom === undefined && right === undefined) {
        throw new Error("Wrong zone xc. The zone cannot be at the same time a full column and a full row");
    }
    return orderedZone;
}
/**
 * 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 2, Left: 1, Right: 1
 *    "Sheet1!A1" ==> Top 0, Bottom 0, Left: 0, Right: 0
 *    "Sheet1!B1:B3" ==> Top 0, Bottom 2, 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;
}
function isXcValid(xc) {
    const zone = toUnboundedZone(xc);
    return isZoneValid(zone);
}
/**
 * Check that the given string is a correct xc representation (ie a valid zone). The try-catch
 * added over the ixXcValid call is necessary because the function can throw an error if the
 * string is not convertible to a zone by the toUnboundedZone function.
 */
function isXcRepresentation(xc) {
    try {
        return isXcValid(xc);
    }
    catch (e) {
        return false;
    }
}
/**
 * Check that the zone has valid coordinates and in
 * the correct order.
 */
function isZoneValid(zone) {
    // Typescript *should* prevent this kind of errors but
    // it's better to be on the safe side at runtime as well.
    const { bottom, top, left, right } = zone;
    if ((bottom !== undefined && isNaN(bottom)) ||
        isNaN(top) ||
        isNaN(left) ||
        (right !== undefined && isNaN(right))) {
        return false;
    }
    return isZoneOrdered(zone) && zone.top >= 0 && zone.left >= 0;
}
/**
 * Check that the zone properties are in the correct order.
 */
function isZoneOrdered(zone) {
    return ((zone.bottom === undefined || (zone.bottom >= zone.top && zone.bottom >= 0)) &&
        (zone.right === undefined || (zone.right >= zone.left && zone.right >= 0)));
}
/**
 * Convert from zone to a cartesian reference
 *
 */
function zoneToXc(zone) {
    const { top, bottom, left, right } = zone;
    const hasHeader = "hasHeader" in zone ? zone.hasHeader : false;
    const isOneCell = top === bottom && left === right;
    if (bottom === undefined && right !== undefined) {
        return top === 0 && !hasHeader
            ? `${numberToLetters(left)}:${numberToLetters(right)}`
            : `${toXC(left, top)}:${numberToLetters(right)}`;
    }
    else if (right === undefined && bottom !== undefined) {
        return left === 0 && !hasHeader
            ? `${top + 1}:${bottom + 1}`
            : `${toXC(left, top)}:${bottom + 1}`;
    }
    else if (bottom !== undefined && right !== undefined) {
        return isOneCell ? toXC(left, top) : `${toXC(left, top)}:${toXC(right, bottom)}`;
    }
    throw new Error("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 (const 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 (const 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 union of multiple unbounded zones.
 */
function unionUnboundedZones(...zones) {
    return {
        top: Math.min(...zones.map((zone) => zone.top)),
        left: Math.min(...zones.map((zone) => zone.left)),
        bottom: zones.some((zone) => zone.bottom === undefined)
            ? undefined
            : Math.max(...zones.map((zone) => zone.bottom)),
        right: zones.some((zone) => zone.right === undefined)
            ? undefined
            : 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;
}
/**
 * Returns true if any two zones in the given list overlap.
 */
function hasOverlappingZones(zones) {
    for (let i = 0; i < zones.length - 1; i++) {
        for (let j = i + 1; j < zones.length; j++) {
            if (overlap(zones[i], zones[j])) {
                return true;
            }
        }
    }
    return false;
}
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 (smallZone.left >= biggerZone.left &&
        smallZone.right <= biggerZone.right &&
        smallZone.top >= biggerZone.top &&
        smallZone.bottom <= biggerZone.bottom);
}
function zoneToDimension(zone) {
    return {
        numberOfRows: zone.bottom - zone.top + 1,
        numberOfCols: zone.right - zone.left + 1,
    };
}
function isOneDimensional(zone) {
    const { numberOfCols, numberOfRows } = zoneToDimension(zone);
    return numberOfCols === 1 || numberOfRows === 1;
}
function excludeTopLeft(zone) {
    const { top, left, bottom, right } = zone;
    if (getZoneArea(zone) === 1) {
        return [];
    }
    const leftColumnZone = {
        top: top + 1,
        bottom,
        left,
        right: left,
    };
    if (right === left) {
        return [leftColumnZone];
    }
    const rightPartZone = {
        top,
        bottom,
        left: left + 1,
        right,
    };
    if (top === bottom) {
        return [rightPartZone];
    }
    return [leftColumnZone, rightPartZone];
}
/**
 * Array of all positions in the zone.
 */
function positions(zone) {
    const positions = [];
    const { left, right, top, bottom } = reorderZone(zone);
    for (const col of range(left, right + 1)) {
        for (const row of range(top, bottom + 1)) {
            positions.push({ col, row });
        }
    }
    return positions;
}
/**
 * Array of all cell positions in the zone.
 */
function cellPositions(sheetId, zone) {
    const positions = [];
    const { left, right, top, bottom } = reorderZone(zone);
    for (const col of range(left, right + 1)) {
        for (const row of range(top, bottom + 1)) {
            positions.push({ sheetId, col, row });
        }
    }
    return positions;
}
function reorderZone(zone) {
    if (zone.right !== undefined && zone.left > zone.right) {
        zone = { ...zone, left: zone.right, right: zone.left };
    }
    if (zone.bottom !== undefined && zone.top > zone.bottom) {
        zone = { ...zone, top: zone.bottom, bottom: zone.top };
    }
    return zone;
}
/**
 * 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 positionToZone(position) {
    return { left: position.col, right: position.col, top: position.row, bottom: position.row };
}
/** Transform a zone into a zone with only its top-left position */
function zoneToTopLeft(zone) {
    return { ...zone, right: zone.left, bottom: zone.top };
}
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);
}
/**
 * Checks if a single zone crosses any of the frozen panes based on the vertical and horizontal split.
 */
function doesZoneCrossFrozenPane(zone, xSplit, ySplit) {
    return ((zone.left < xSplit && xSplit <= zone.right) || (zone.top < ySplit && ySplit <= zone.bottom));
}
/**
 * Checks if any of the given zones crosses any of the frozen panes.
 */
function doesAnyZoneCrossFrozenPane(zones, xSplit, ySplit) {
    return zones.some((zone) => doesZoneCrossFrozenPane(zone, xSplit, ySplit));
}
function boundUnboundedZone(unboundedZone, sheetSize) {
    const { left, top, bottom, right } = unboundedZone;
    if (right !== undefined && bottom !== undefined) {
        return unboundedZone;
    }
    else if (bottom === undefined && right !== undefined) {
        return { right, top, left, bottom: sheetSize.numberOfRows - 1 };
    }
    else if (right === undefined && bottom !== undefined) {
        return { bottom, left, top, right: sheetSize.numberOfCols - 1 };
    }
    throw new Error("Bad zone format");
}
/**
 * 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).length === 1;
}
/** Return all the columns in the given list of zones */
function getZonesCols(zones) {
    const set = new Set();
    for (const zone of recomputeZones(zones)) {
        for (const col of range(zone.left, zone.right + 1)) {
            set.add(col);
        }
    }
    return set;
}
/**
 * Returns one merged zone per column,
 * spanning the full vertical range across all input zones.
 */
function getZonesByColumns(zones) {
    const map = new Map();
    for (const zone of zones) {
        for (let col = zone.left; col <= zone.right; col++) {
            const existing = map.get(col);
            if (existing) {
                existing.top = Math.min(existing.top, zone.top);
                existing.bottom = Math.max(existing.bottom, zone.bottom);
            }
            else {
                map.set(col, { left: col, right: col, top: zone.top, bottom: zone.bottom });
            }
        }
    }
    return Array.from(map.values());
}
/** Return all the rows in the given list of zones */
function getZonesRows(zones) {
    const set = new Set();
    for (const zone of recomputeZones(zones)) {
        for (const row of range(zone.top, zone.bottom + 1)) {
            set.add(row);
        }
    }
    return set;
}
/**
 * Check if two zones are contiguous, ie. that they share a border
 */
function areZoneContiguous(zone1, zone2) {
    if (zone1.right + 1 === zone2.left || zone1.left === zone2.right + 1) {
        return ((zone1.top <= zone2.bottom && zone1.top >= zone2.top) ||
            (zone2.top <= zone1.bottom && zone2.top >= zone1.top));
    }
    if (zone1.bottom + 1 === zone2.top || zone1.top === zone2.bottom + 1) {
        return ((zone1.left <= zone2.right && zone1.left >= zone2.left) ||
            (zone2.left <= zone1.right && zone2.left >= zone1.left));
    }
    return false;
}
/**
 * Merge contiguous and overlapping zones that are in the array into bigger zones
 */
function mergeContiguousZones(zones) {
    const mergedZones = [...zones];
    let hasMerged = true;
    while (hasMerged) {
        hasMerged = false;
        for (let i = 0; i < mergedZones.length; i++) {
            const zone = mergedZones[i];
            const mergeableZoneIndex = mergedZones.findIndex((z, j) => i !== j && (areZoneContiguous(z, zone) || overlap(z, zone)));
            if (mergeableZoneIndex !== -1) {
                mergedZones[i] = union(mergedZones[mergeableZoneIndex], zone);
                mergedZones.splice(mergeableZoneIndex, 1);
                hasMerged = true;
                break;
            }
        }
    }
    return mergedZones;
}

const globalReverseLookup$1 = new WeakMap();
const globalIdCounter = new WeakMap();
/**
 * Get the id of the given item (its key in the given dictionary).
 * If the given item does not exist in the dictionary, it creates one with a new id.
 */
function getItemId(item, itemsDic) {
    if (!globalReverseLookup$1.has(itemsDic)) {
        globalReverseLookup$1.set(itemsDic, new Map());
        globalIdCounter.set(itemsDic, 0);
    }
    const reverseLookup = globalReverseLookup$1.get(itemsDic);
    const canonical = getCanonicalRepresentation(item);
    if (reverseLookup.has(canonical)) {
        const id = reverseLookup.get(canonical);
        itemsDic[id] = item;
        return id;
    }
    // Generate new Id if the item didn't exist in the dictionary
    const newId = globalIdCounter.get(itemsDic) + 1;
    reverseLookup.set(canonical, newId);
    globalIdCounter.set(itemsDic, newId);
    itemsDic[newId] = item;
    return newId;
}
function groupItemIdsByZones(positionsByItemId) {
    const result = {};
    for (const itemId in positionsByItemId) {
        const zones = recomputeZones(positionsByItemId[itemId].map(positionToZone));
        for (const zone of zones) {
            result[zoneToXc(zone)] = Number(itemId);
        }
    }
    return result;
}
function* iterateItemIdsPositions(sheetId, itemIdsByZones) {
    for (const zoneXc in itemIdsByZones) {
        const zone = toZone(zoneXc);
        const itemId = itemIdsByZones[zoneXc];
        for (let row = zone.top; row <= zone.bottom; row++) {
            for (let col = zone.left; col <= zone.right; col++) {
                const position = { sheetId, col, row };
                yield [position, itemId];
            }
        }
    }
}
function getCanonicalRepresentation(item) {
    if (item === null)
        return "null";
    if (item === undefined)
        return "undefined";
    if (typeof item !== "object")
        return String(item);
    if (Array.isArray(item)) {
        const len = item.length;
        let result = "[";
        for (let i = 0; i < len; i++) {
            if (i > 0)
                result += ",";
            result += getCanonicalRepresentation(item[i]);
        }
        return result + "]";
    }
    const keys = Object.keys(item).sort();
    let repr = "{";
    for (const key of keys) {
        if (item[key] !== undefined) {
            repr += `"${key}":${getCanonicalRepresentation(item[key])},`;
        }
    }
    repr += "}";
    return repr;
}

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

/** 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) {
    if (!ref.includes("!")) {
        return { xc: ref };
    }
    const parts = ref.split("!");
    const xc = parts.pop();
    const sheetName = getUnquotedSheetName(parts.join("!")) || undefined;
    return { sheetName, xc };
}
/** Return a reference SheetName!xc from the given arguments */
function getFullReference(sheetName, xc) {
    return sheetName !== undefined ? `${getCanonicalSymbolName(sheetName)}!${xc}` : xc;
}

function createRange(args, getSheetSize) {
    const unboundedZone = args.zone;
    const zone = boundUnboundedZone(unboundedZone, getSheetSize(args.sheetId));
    let parts = args.parts;
    if (args.parts.length === 1 && getZoneArea(zone) > 1) {
        parts = [args.parts[0], args.parts[0]];
    }
    else if (args.parts.length === 2 && getZoneArea(zone) === 1) {
        parts = [args.parts[0]];
    }
    return {
        unboundedZone,
        zone,
        parts,
        prefixSheet: args.prefixSheet,
        invalidSheetName: args.invalidSheetName,
        sheetId: args.sheetId,
    };
}
/**
 * Create a range from a string XC: A1, Sheet1!A1
 * The XC is expected to be valid.
 */
function createRangeFromXc(args, getSheetSize) {
    const fullXc = args.xc;
    const { xc, sheetName } = splitReference(fullXc);
    const unboundedZone = toUnboundedZone(xc);
    const parts = getRangeParts(xc, unboundedZone);
    return createRange({
        zone: unboundedZone,
        parts,
        sheetId: args.sheetId,
        prefixSheet: Boolean(sheetName),
        invalidSheetName: args.invalidSheetName,
    }, getSheetSize);
}
function createInvalidRange(sheetXC) {
    const invalidZone = { left: -1, top: -1, right: -1, bottom: -1 };
    return {
        sheetId: "",
        zone: invalidZone,
        unboundedZone: invalidZone,
        parts: [],
        invalidXc: sheetXC,
        prefixSheet: false,
    };
}
function isFullColRange(range) {
    return isFullCol(range.unboundedZone);
}
function isFullRowRange(range) {
    return isFullRow(range.unboundedZone);
}
function getRangeString(range, forSheetId, getSheetName, options = { useBoundedReference: false, useFixedReference: false }) {
    if (range.invalidXc) {
        return range.invalidXc;
    }
    if (range.zone.bottom - range.zone.top < 0 || range.zone.right - range.zone.left < 0) {
        return CellErrorType.InvalidReference;
    }
    if (range.zone.left < 0 || range.zone.top < 0) {
        return CellErrorType.InvalidReference;
    }
    const prefixSheet = range.sheetId !== forSheetId || range.invalidSheetName || range.prefixSheet;
    let sheetName = "";
    if (prefixSheet) {
        if (range.invalidSheetName) {
            sheetName = getCanonicalSymbolName(range.invalidSheetName);
        }
        else {
            sheetName = getCanonicalSymbolName(getSheetName(range.sheetId));
        }
    }
    if (prefixSheet && !sheetName) {
        return CellErrorType.InvalidReference;
    }
    let rangeString = getRangePartString(range, 0, options);
    if (range.parts && range.parts.length === 2) {
        // range if converts A2:A2 into A2 except if any part of the original range had fixed row or column (with $)
        if (range.zone.top !== range.zone.bottom ||
            range.zone.left !== range.zone.right ||
            range.parts[0].rowFixed ||
            range.parts[0].colFixed ||
            range.parts[1].rowFixed ||
            range.parts[1].colFixed) {
            rangeString += ":";
            rangeString += getRangePartString(range, 1, options);
        }
    }
    return `${prefixSheet ? sheetName + "!" : ""}${rangeString}`;
}
/**
 * Duplicate a range. If the range is on the sheetIdFrom, the range will target
 * sheetIdTo.
 */
function duplicateRangeInDuplicatedSheet(sheetIdFrom, sheetIdTo, range) {
    const sheetId = range.sheetId === sheetIdFrom ? sheetIdTo : range.sheetId;
    return { ...range, 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;
}
/**
 * Get all the cell positions in the given ranges. If a cell is in multiple ranges, it will be returned multiple times.
 */
function getCellPositionsInRanges(ranges) {
    const cellPositions = [];
    for (const range of ranges) {
        for (const position of positions(range.zone)) {
            cellPositions.push({ ...position, sheetId: range.sheetId });
        }
    }
    return cellPositions;
}
function 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;
}
function positionToBoundedRange(position) {
    const zone = { left: position.col, top: position.row, right: position.col, bottom: position.row };
    return { sheetId: position.sheetId, zone };
}
/**
 * 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
 */
function orderRange(range) {
    if (isZoneOrdered(range.zone)) {
        return range;
    }
    const unboundedZone = { ...range.unboundedZone };
    const zone = { ...range.zone };
    let parts = range.parts;
    if (unboundedZone.right !== undefined && unboundedZone.right < unboundedZone.left) {
        const right = unboundedZone.right;
        unboundedZone.right = unboundedZone.left;
        unboundedZone.left = right;
        zone.right = zone.left;
        zone.left = right;
        parts = [
            {
                colFixed: parts[1]?.colFixed || false,
                rowFixed: parts[0]?.rowFixed || false,
            },
            {
                colFixed: parts[0]?.colFixed || false,
                rowFixed: parts[1]?.rowFixed || false,
            },
        ];
    }
    if (unboundedZone.bottom !== undefined && unboundedZone.bottom < unboundedZone.top) {
        const bottom = unboundedZone.bottom;
        unboundedZone.bottom = unboundedZone.top;
        unboundedZone.top = bottom;
        zone.bottom = zone.top;
        zone.top = bottom;
        parts = [
            {
                colFixed: parts[0]?.colFixed || false,
                rowFixed: parts[1]?.rowFixed || false,
            },
            {
                colFixed: parts[1]?.colFixed || false,
                rowFixed: parts[0]?.rowFixed || false,
            },
        ];
    }
    return {
        unboundedZone,
        zone,
        parts,
        invalidXc: range.invalidXc,
        prefixSheet: range.prefixSheet,
        invalidSheetName: range.invalidSheetName,
        sheetId: range.sheetId,
    };
}
function getRangeAdapter(cmd) {
    return rangeAdapterRegistry.get(cmd.type)?.(cmd);
}
class RangeAdapterRegistry extends Registry {
    add(cmdType, fn) {
        super.add(cmdType, fn);
        return this;
    }
    get(cmdType) {
        return this.content[cmdType];
    }
}
const rangeAdapterRegistry = new RangeAdapterRegistry();
rangeAdapterRegistry
    .add("REMOVE_COLUMNS_ROWS", (cmd) => ({
    applyChange: getApplyRangeChangeRemoveColRow(cmd),
    sheetId: cmd.sheetId,
    sheetName: { old: cmd.sheetName, current: cmd.sheetName },
}))
    .add("ADD_COLUMNS_ROWS", (cmd) => ({
    applyChange: getApplyRangeChangeAddColRow(cmd),
    sheetId: cmd.sheetId,
    sheetName: { old: cmd.sheetName, current: cmd.sheetName },
}))
    .add("DELETE_SHEET", (cmd) => ({
    applyChange: getApplyRangeChangeDeleteSheet(cmd),
    sheetId: cmd.sheetId,
    sheetName: { old: cmd.sheetName, current: cmd.sheetName },
}))
    .add("RENAME_SHEET", (cmd) => ({
    applyChange: getApplyRangeChangeRenameSheet(cmd),
    sheetId: cmd.sheetId,
    sheetName: { old: cmd.oldName, current: cmd.newName },
}))
    .add("MOVE_RANGES", (cmd) => ({
    applyChange: getApplyRangeChangeMoveRange(cmd),
    sheetId: cmd.sheetId,
    sheetName: { old: cmd.sheetName, current: cmd.sheetName },
}));
function getApplyRangeChangeRemoveColRow(cmd) {
    const start = cmd.dimension === "COL" ? "left" : "top";
    const end = cmd.dimension === "COL" ? "right" : "bottom";
    const dimension = cmd.dimension === "COL" ? "columns" : "rows";
    const elements = [...cmd.elements];
    elements.sort((a, b) => b - a);
    const groups = groupConsecutive(elements);
    return (range) => {
        if (range.sheetId !== cmd.sheetId) {
            return { changeType: "NONE", range };
        }
        let newRange = range;
        let changeType = "NONE";
        for (const 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 = createAdaptedRange(newRange, dimension, changeType, -toRemove);
            }
            else if (range.zone[start] >= min && range.zone[end] <= max) {
                changeType = "REMOVE";
                newRange = createInvalidRange(CellErrorType.InvalidReference);
            }
            else if (range.zone[start] <= max && range.zone[end] >= max) {
                const toRemove = max - range.zone[start] + 1;
                changeType = "RESIZE";
                newRange = createAdaptedRange(newRange, dimension, changeType, -toRemove);
                newRange = createAdaptedRange(newRange, dimension, "MOVE", -(range.zone[start] - min));
            }
            else if (min < range.zone[start]) {
                changeType = "MOVE";
                newRange = createAdaptedRange(newRange, dimension, changeType, -(max - min + 1));
            }
        }
        return { changeType, range: newRange };
    };
}
function getApplyRangeChangeAddColRow(cmd) {
    const start = cmd.dimension === "COL" ? "left" : "top";
    const end = cmd.dimension === "COL" ? "right" : "bottom";
    const dimension = cmd.dimension === "COL" ? "columns" : "rows";
    return (range) => {
        if (range.sheetId !== cmd.sheetId) {
            return { changeType: "NONE", range };
        }
        if (cmd.position === "after") {
            if (range.zone[start] <= cmd.base && cmd.base < range.zone[end]) {
                return {
                    changeType: "RESIZE",
                    range: createAdaptedRange(range, dimension, "RESIZE", cmd.quantity),
                };
            }
            if (cmd.base < range.zone[start]) {
                return {
                    changeType: "MOVE",
                    range: createAdaptedRange(range, dimension, "MOVE", cmd.quantity),
                };
            }
        }
        else {
            if (range.zone[start] < cmd.base && cmd.base <= range.zone[end]) {
                return {
                    changeType: "RESIZE",
                    range: createAdaptedRange(range, dimension, "RESIZE", cmd.quantity),
                };
            }
            if (cmd.base <= range.zone[start]) {
                return {
                    changeType: "MOVE",
                    range: createAdaptedRange(range, dimension, "MOVE", cmd.quantity),
                };
            }
        }
        return { changeType: "NONE", range };
    };
}
function getApplyRangeChangeDeleteSheet(cmd) {
    return (range) => {
        if (range.sheetId !== cmd.sheetId && range.invalidSheetName !== cmd.sheetName) {
            return { changeType: "NONE", range };
        }
        const invalidSheetName = cmd.sheetName;
        range = {
            ...createInvalidRange(CellErrorType.InvalidReference),
            invalidSheetName,
        };
        return { changeType: "REMOVE", range };
    };
}
function getApplyRangeChangeRenameSheet(cmd) {
    return (range) => {
        if (range.sheetId === cmd.sheetId) {
            return { changeType: "CHANGE", range };
        }
        if (cmd.newName && range.invalidSheetName === cmd.newName) {
            const invalidSheetName = undefined;
            const sheetId = cmd.sheetId;
            const newRange = { ...range, sheetId, invalidSheetName };
            return { changeType: "CHANGE", range: newRange };
        }
        if (cmd.oldName && range.invalidSheetName === cmd.oldName) {
            const invalidSheetName = undefined;
            const sheetId = cmd.sheetId;
            const newRange = { ...range, sheetId, invalidSheetName };
            return { changeType: "CHANGE", range: newRange };
        }
        return { changeType: "NONE", range };
    };
}
function getApplyRangeChangeMoveRange(cmd) {
    const originZone = cmd.target[0];
    return (range) => {
        if (range.sheetId !== cmd.sheetId || !isZoneInside(range.zone, originZone)) {
            return { changeType: "NONE", range };
        }
        const targetSheetId = cmd.targetSheetId;
        const offsetX = cmd.col - originZone.left;
        const offsetY = cmd.row - originZone.top;
        const adaptedRange = createAdaptedRange(range, "both", "MOVE", [offsetX, offsetY]);
        const prefixSheet = cmd.sheetId === targetSheetId ? adaptedRange.prefixSheet : true;
        return {
            changeType: "MOVE",
            range: { ...adaptedRange, sheetId: targetSheetId, prefixSheet },
        };
    };
}
function createAdaptedRange(range, dimension, operation, by) {
    return {
        ...range,
        unboundedZone: createAdaptedZone(range.unboundedZone, dimension, operation, by),
        zone: createAdaptedZone(range.zone, dimension, operation, by),
    };
}
function getRangePartString(range, part, options = { useBoundedReference: false, useFixedReference: false }) {
    const colFixed = range.parts[part]?.colFixed || options.useFixedReference ? "$" : "";
    const col = part === 0 ? numberToLetters(range.zone.left) : numberToLetters(range.zone.right);
    const rowFixed = range.parts[part]?.rowFixed || options.useFixedReference ? "$" : "";
    const row = part === 0 ? String(range.zone.top + 1) : String(range.zone.bottom + 1);
    let str = "";
    if (isFullCol(range.unboundedZone) && !options.useBoundedReference) {
        if (part === 0 && range.unboundedZone.hasHeader) {
            str = colFixed + col + rowFixed + row;
        }
        else {
            str = colFixed + col;
        }
    }
    else if (isFullRow(range.unboundedZone) && !options.useBoundedReference) {
        if (part === 0 && range.unboundedZone.hasHeader) {
            str = colFixed + col + rowFixed + row;
        }
        else {
            str = rowFixed + row;
        }
    }
    else {
        str = colFixed + col + rowFixed + row;
    }
    return str;
}

/** 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;
    const len = str.length;
    let patternIndex = 0;
    for (let i = 0; i < len; i++) {
        if (str[i] === pattern[patternIndex]) {
            patternIndex++;
            currentScore += 100 + currentScore - i / 200;
        }
        else {
            currentScore = 0;
        }
        totalScore = totalScore + currentScore;
    }
    return patternIndex === pattern.length ? totalScore : 0;
}
/**
 * Return a list of things that matches a pattern, ordered by their 'score' (
 * higher score first). An higher score means that the match is better. For
 * example, consecutive letters are considered a better match.
 */
function fuzzyLookup(pattern, list, fn) {
    const results = [];
    list.forEach((data) => {
        const score = fuzzyMatch(pattern, fn(data));
        if (score > 0) {
            results.push({ score, elem: data });
        }
    });
    // we want better matches first
    results.sort((a, b) => b.score - a.score);
    return results.map((r) => r.elem);
}

function createDefaultRows(rowNumber) {
    const rows = [];
    for (let i = 0; i < rowNumber; i++) {
        const row = {
            cells: {},
        };
        rows.push(row);
    }
    return rows;
}
function moveHeaderIndexesOnHeaderAddition(indexHeaderAdded, numberAdded, headers) {
    return headers.map((header) => {
        if (header >= indexHeaderAdded) {
            return header + numberAdded;
        }
        return header;
    });
}
function moveHeaderIndexesOnHeaderDeletion(deletedHeaders, headers) {
    deletedHeaders = [...deletedHeaders].sort((a, b) => b - a);
    return headers
        .map((header) => {
        for (const deletedHeader of deletedHeaders) {
            if (header > deletedHeader) {
                header--;
            }
            else if (header === deletedHeader) {
                return undefined;
            }
        }
        return header;
    })
        .filter(isDefined);
}
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 = _t("Copy of %s", nameToDuplicate);
    let name = baseName.toString();
    while (existingNames.includes(name)) {
        name = `${baseName} (${i})`;
        i++;
    }
    return name;
}
const toStandardizedSheetName = memoize(function toStandardizedSheetName(name) {
    return getUnquotedSheetName(name.trim().toUpperCase());
});
function isSheetNameEqual(name1, name2) {
    if (name1 === undefined || name2 === undefined) {
        return false;
    }
    return toStandardizedSheetName(name1) === toStandardizedSheetName(name2);
}

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

/*
 * https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
 * */
class UuidGenerator {
    /**
     * Generates a custom UUID using a simple 36^12 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 (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) {
                const 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 (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) {
                const r = (Math.random() * 16) | 0, v = c === "x" ? r : (r & 0x3) | 0x8;
                return v.toString(16);
            });
        }
    }
}

const AllowedImageMimeTypes = [
    "image/avif",
    "image/bmp",
    "image/gif",
    "image/vnd.microsoft.icon",
    "image/jpeg",
    "image/png",
    "image/tiff",
    "image/webp",
];

function getClipboardDataPositions(sheetId, zones) {
    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 columnsIndexes = [...new Set(cellsPosition.map((p) => p.col))].sort((a, b) => a - b);
    const rowsIndexes = [...new Set(cellsPosition.map((p) => p.row))].sort((a, b) => a - b);
    return { sheetId, zones, clippedZones, columnsIndexes, rowsIndexes };
}
/**
 * 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.
 */
function splitZoneForPaste(selection, splitWidth, splitHeight) {
    const right = Math.max(selection.right - splitWidth + 1, selection.left);
    const bottom = Math.max(selection.bottom - splitHeight + 1, selection.top);
    const zones = [];
    for (let left = selection.left; left <= right; left += splitWidth) {
        for (let top = selection.top; top <= bottom; top += splitHeight) {
            zones.push({
                left,
                top,
                bottom: top + splitHeight - 1,
                right: left + splitWidth - 1,
            });
        }
    }
    return zones;
}
/**
 * Compute the complete zones where to paste the current clipboard
 */
function getPasteZones(target, content) {
    if (!content.length || !content[0].length) {
        return target;
    }
    const width = content[0].length, height = content.length;
    return target.map((t) => splitZoneForPaste(t, width, height)).flat();
}
function parseOSClipboardContent(content) {
    let spreadsheetContent = undefined;
    if (content[ClipboardMIMEType.Html]) {
        const htmlDocument = new DOMParser().parseFromString(content[ClipboardMIMEType.Html], "text/html");
        spreadsheetContent = getOSheetDataFromHTML(htmlDocument);
    }
    const textContent = content[ClipboardMIMEType.PlainText] || "";
    let imageBlob = undefined;
    if (!textContent.trim()) {
        for (const type of AllowedImageMimeTypes) {
            if (content[type]) {
                imageBlob = content[type];
                break;
            }
        }
    }
    const osClipboardContent = {
        text: textContent,
        data: spreadsheetContent,
        imageBlob,
    };
    return osClipboardContent;
}
function getOSheetDataFromHTML(htmlDocument) {
    const attributes = [...htmlDocument.documentElement.attributes];
    // Check if it's a Microsoft Office clipboard data (it will have some namespaces defined in the root element)
    if (attributes.some((attr) => attr.value.includes("microsoft"))) {
        return undefined;
    }
    const oSheetClipboardData = htmlDocument
        .querySelector("div")
        ?.getAttribute("data-osheet-clipboard");
    return oSheetClipboardData && JSON.parse(oSheetClipboardData);
}
/**
 * Applies each clipboard handler to paste its corresponding data into the target.
 */
const applyClipboardHandlersPaste = (handlers, copiedData, target, options) => {
    handlers.forEach(({ handlerName, handler }) => {
        const data = copiedData[handlerName];
        if (data) {
            handler.paste(target, data, options);
        }
    });
};
/**
 * Returns the paste target based on clipboard handlers.
 * Also includes the full affected zone and the list of pasted zones for selection.
 */
function getPasteTargetFromHandlers(sheetId, zones, copiedData, handlers, options) {
    let zone = undefined;
    const selectedZones = [];
    const target = {
        sheetId,
        zones,
    };
    for (const { handlerName, handler } of handlers) {
        const handlerData = copiedData[handlerName];
        if (!handlerData) {
            continue;
        }
        const currentTarget = handler.getPasteTarget(sheetId, zones, handlerData, options);
        if (currentTarget.figureId) {
            target.figureId = currentTarget.figureId;
        }
        for (const targetZone of currentTarget.zones) {
            selectedZones.push(targetZone);
            if (zone === undefined) {
                zone = targetZone;
                continue;
            }
            zone = union(zone, targetZone);
        }
    }
    return {
        target,
        zone,
        selectedZones,
    };
}
/**
 * Updates the selection after a paste operation.
 */
const selectPastedZone = (selection, sourceZones, pastedZones) => {
    const anchorCell = {
        col: sourceZones[0].left,
        row: sourceZones[0].top,
    };
    selection.getBackToDefault();
    selection.selectZone({ cell: anchorCell, zone: union(...pastedZones) }, { scrollIntoView: false });
};

class ClipboardHandler {
    getters;
    dispatch;
    constructor(getters, dispatch) {
        this.getters = getters;
        this.dispatch = dispatch;
    }
    copy(data, isCutOperation, mode = "copyPaste") {
        return;
    }
    paste(target, clippedContent, options) { }
    isPasteAllowed(sheetId, target, content, option) {
        return "Success" /* CommandResult.Success */;
    }
    isCutAllowed(data) {
        return "Success" /* CommandResult.Success */;
    }
    getPasteTarget(sheetId, target, content, options) {
        return { zones: [], sheetId };
    }
    convertTextToClipboardData(data) {
        return;
    }
}

class AbstractCellClipboardHandler extends ClipboardHandler {
    copy(data, isCutOperation, mode = "copyPaste") {
        return;
    }
    pasteFromCopy(sheetId, target, content, 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.
            for (const zone of getPasteZones(target, content)) {
                this.pasteZone(sheetId, zone.left, zone.top, content, options);
            }
        }
        else {
            // in this case, due to the isPasteAllowed function: state.cells contains
            // only one cell
            for (const zone of recomputeZones(target)) {
                for (let col = zone.left; col <= zone.right; col++) {
                    for (let row = zone.top; row <= zone.bottom; row++) {
                        this.pasteZone(sheetId, col, row, content, options);
                    }
                }
            }
        }
    }
    pasteZone(sheetId, col, row, data, clipboardOptions) { }
}

class BorderClipboardHandler extends AbstractCellClipboardHandler {
    queuedBordersToAdd = {};
    copy(data) {
        const sheetId = data.sheetId;
        if (data.zones.length === 0) {
            return;
        }
        const { rowsIndexes, columnsIndexes } = data;
        const borders = [];
        for (const row of rowsIndexes) {
            const bordersInRow = [];
            for (const col of columnsIndexes) {
                const position = { col, row, sheetId };
                bordersInRow.push(this.getters.getCellBorder(position));
            }
            borders.push(bordersInRow);
        }
        return { borders };
    }
    paste(target, content, options) {
        const sheetId = target.sheetId;
        if (options.pasteOption === "asValue") {
            return;
        }
        const zones = target.zones;
        if (!options.isCutOperation) {
            this.pasteFromCopy(sheetId, zones, content.borders);
        }
        else {
            const { left, top } = zones[0];
            this.pasteZone(sheetId, left, top, content.borders);
        }
        this.executeQueuedChanges(sheetId);
    }
    pasteZone(sheetId, col, row, borders) {
        for (const [r, rowBorders] of borders.entries()) {
            for (const [c, originBorders] of rowBorders.entries()) {
                const position = { col: col + c, row: row + r, sheetId };
                this.pasteBorder(originBorders, position);
            }
        }
    }
    /**
     * Paste the border at the given position to the target position
     */
    pasteBorder(originBorders, target) {
        const targetBorders = this.getters.getCellBorder(target);
        const border = {
            ...targetBorders,
            ...originBorders,
        };
        const borderKey = JSON.stringify(border);
        if (!this.queuedBordersToAdd[borderKey]) {
            this.queuedBordersToAdd[borderKey] = [];
        }
        this.queuedBordersToAdd[borderKey].push(positionToZone(target));
    }
    executeQueuedChanges(pasteSheetTarget) {
        for (const borderKey in this.queuedBordersToAdd) {
            const zones = this.queuedBordersToAdd[borderKey];
            const border = JSON.parse(borderKey);
            const target = recomputeZones(zones, []);
            this.dispatch("SET_BORDERS_ON_TARGET", { sheetId: pasteSheetTarget, target, border });
        }
        this.queuedBordersToAdd = {};
    }
}

class AbstractFigureClipboardHandler extends ClipboardHandler {
    copy(data) {
        return;
    }
}

class CarouselClipboardHandler extends AbstractFigureClipboardHandler {
    copy(data) {
        const sheetId = data.sheetId;
        const figure = this.getters.getFigure(sheetId, data.figureId);
        if (!figure) {
            throw new Error(`No figure for the given id: ${data.figureId}`);
        }
        if (figure.tag !== "carousel") {
            return;
        }
        const copiedFigure = { ...figure };
        const copiedCarousel = this.getters.getCarousel(data.figureId);
        const copiedCharts = {};
        for (const item of copiedCarousel.items) {
            if (item.type === "chart") {
                const chart = this.getters.getChart(item.chartId);
                if (!chart) {
                    throw new Error(`No chart for the given id: ${item.chartId}`);
                }
                copiedCharts[item.chartId] = chart.copyInSheetId(sheetId);
            }
        }
        return {
            figureId: data.figureId,
            copiedFigure,
            copiedCarousel,
            copiedCharts,
            copiedSheetId: sheetId,
        };
    }
    getPasteTarget(sheetId) {
        const newId = new UuidGenerator().smallUuid();
        return { zones: [], figureId: newId, sheetId };
    }
    paste(target, clippedContent, options) {
        if (!target.figureId) {
            return;
        }
        const { zones, figureId } = target;
        const sheetId = target.sheetId;
        const { width, height } = clippedContent.copiedFigure;
        const maxPosition = this.getters.getMaxAnchorOffset(sheetId, height, width);
        let { left: col, top: row } = zones[0];
        const offset = { x: 0, y: 0 };
        if (col > maxPosition.col) {
            col = maxPosition.col;
            offset.x = maxPosition.offset.x;
        }
        if (row > maxPosition.row) {
            row = maxPosition.row;
            offset.y = maxPosition.offset.y;
        }
        this.dispatch("CREATE_CAROUSEL", {
            figureId,
            sheetId,
            definition: { items: [] },
            col,
            row,
            offset,
            size: { height, width },
        });
        const uuidGenerator = new UuidGenerator();
        const items = deepCopy(clippedContent.copiedCarousel.items);
        for (const item of items) {
            if (item.type !== "chart") {
                continue;
            }
            const chart = clippedContent.copiedCharts[item.chartId];
            const newId = uuidGenerator.smallUuid();
            const definition = chart.copyInSheetId(sheetId).getDefinition();
            this.dispatch("CREATE_CHART", { figureId, chartId: newId, sheetId, definition });
            item.chartId = newId;
        }
        this.dispatch("UPDATE_CAROUSEL", {
            sheetId,
            figureId,
            definition: { ...clippedContent.copiedCarousel, items },
        });
        if (options.isCutOperation) {
            this.dispatch("DELETE_FIGURE", {
                sheetId: clippedContent.copiedSheetId,
                figureId: clippedContent.copiedFigure.id,
            });
        }
        this.dispatch("SELECT_FIGURE", { figureId });
    }
    isPasteAllowed(sheetId, target, content, option) {
        if (target.length === 0) {
            return "EmptyTarget" /* CommandResult.EmptyTarget */;
        }
        if (option?.pasteOption !== undefined) {
            return "WrongFigurePasteOption" /* CommandResult.WrongFigurePasteOption */;
        }
        return "Success" /* CommandResult.Success */;
    }
}

/**
 * Tokenizer
 *
 * A tokenizer is a piece of code whose job is to transform a string into a list
 * of "tokens". For example, "(12+" is converted into:
 *   [{type: "LEFT_PAREN", value: "("},
 *    {type: "NUMBER", value: "12"},
 *    {type: "OPERATOR", value: "+"}]
 *
 * As the example shows, a tokenizer does not care about the meaning behind those
 * tokens. It only cares about the structure.
 *
 * The tokenizer is usually the first step in a compilation pipeline.  Also, it
 * is useful for the composer, which needs to be able to work with incomplete
 * formulas.
 */
const POSTFIX_UNARY_OPERATORS = ["%"];
const OPERATORS = "+,-,*,/,:,=,<>,>=,>,<=,<,^,&".split(",").concat(POSTFIX_UNARY_OPERATORS);
function tokenize(str, locale = DEFAULT_LOCALE) {
    str = replaceNewLines(str);
    const chars = new TokenizingChars(str);
    const result = [];
    const tokenizeSpace = specialWhiteSpaceRegexp.test(str)
        ? tokenizeSpecialCharacterSpace
        : tokenizeSimpleSpace;
    while (!chars.isOver()) {
        let token = tokenizeNewLine(chars) ||
            tokenizeSpace(chars) ||
            tokenizeArgsSeparator(chars, locale) ||
            tokenizeParenthesis(chars) ||
            tokenizeOperator(chars) ||
            tokenizeString(chars) ||
            tokenizeDebugger(chars) ||
            tokenizeInvalidRange(chars) ||
            tokenizeNumber(chars, locale) ||
            tokenizeSymbol(chars);
        if (!token) {
            token = { type: "UNKNOWN", value: chars.shift() };
        }
        result.push(token);
    }
    return result;
}
function tokenizeDebugger(chars) {
    if (chars.current === "?") {
        chars.shift();
        return { type: "DEBUGGER", value: "?" };
    }
    return null;
}
const parenthesis = {
    "(": { type: "LEFT_PAREN", value: "(" },
    ")": { type: "RIGHT_PAREN", value: ")" },
};
function tokenizeParenthesis(chars) {
    if (chars.current === "(" || chars.current === ")") {
        const value = chars.shift();
        return parenthesis[value];
    }
    return null;
}
function tokenizeArgsSeparator(chars, locale) {
    if (chars.current === locale.formulaArgSeparator) {
        const value = chars.shift();
        const type = "ARG_SEPARATOR";
        return { type, value };
    }
    return null;
}
function tokenizeOperator(chars) {
    for (const op of OPERATORS) {
        if (chars.currentStartsWith(op)) {
            chars.advanceBy(op.length);
            return { type: "OPERATOR", value: op };
        }
    }
    return null;
}
const FIRST_POSSIBLE_NUMBER_CHARS = new Set("0123456789");
function tokenizeNumber(chars, locale) {
    if (!FIRST_POSSIBLE_NUMBER_CHARS.has(chars.current) &&
        chars.current !== locale.decimalSeparator) {
        return null;
    }
    const match = chars.remaining().match(getFormulaNumberRegex(locale.decimalSeparator));
    if (match) {
        chars.advanceBy(match[0].length);
        return { type: "NUMBER", value: match[0] };
    }
    return null;
}
function tokenizeString(chars) {
    if (chars.current === '"') {
        const startChar = chars.shift();
        let letters = startChar;
        while (chars.current && (chars.current !== startChar || letters[letters.length - 1] === "\\")) {
            letters += chars.shift();
        }
        if (chars.current === '"') {
            letters += chars.shift();
        }
        return {
            type: "STRING",
            value: letters,
        };
    }
    return null;
}
/**
  - \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.current === "'") {
        let lastChar = chars.shift();
        result += lastChar;
        while (chars.current) {
            lastChar = chars.shift();
            result += lastChar;
            if (lastChar === "'") {
                if (chars.current && chars.current === "'") {
                    lastChar = chars.shift();
                    result += lastChar;
                }
                else {
                    break;
                }
            }
        }
        if (lastChar !== "'") {
            return {
                type: "UNKNOWN",
                value: result,
            };
        }
    }
    while (chars.current &&
        (SYMBOL_CHARS.has(chars.current) || chars.current.match(unicodeSymbolCharRegexp))) {
        result += chars.shift();
    }
    if (result.length) {
        const value = result;
        const isReference = rangeReference.test(value);
        if (isReference) {
            return { type: "REFERENCE", value };
        }
        return { type: "SYMBOL", value };
    }
    return null;
}
function tokenizeSpecialCharacterSpace(chars) {
    let spaces = "";
    while (chars.current === " " || (chars.current && chars.current.match(specialWhiteSpaceRegexp))) {
        spaces += chars.shift();
    }
    if (spaces) {
        return { type: "SPACE", value: spaces };
    }
    return null;
}
function tokenizeSimpleSpace(chars) {
    let spaces = "";
    while (chars.current === " ") {
        spaces += chars.shift();
    }
    if (spaces) {
        return { type: "SPACE", value: spaces };
    }
    return null;
}
function tokenizeNewLine(chars) {
    let length = 0;
    while (chars.current === NEWLINE) {
        length++;
        chars.shift();
    }
    if (length) {
        return { type: "SPACE", value: NEWLINE.repeat(length) };
    }
    return null;
}
function tokenizeInvalidRange(chars) {
    if (chars.currentStartsWith(CellErrorType.InvalidReference)) {
        chars.advanceBy(CellErrorType.InvalidReference.length);
        return { type: "INVALID_REFERENCE", value: CellErrorType.InvalidReference };
    }
    return null;
}

function isValidLocale(locale) {
    if (!locale ||
        typeof locale !== "object" ||
        !(!locale.thousandsSeparator || typeof locale.thousandsSeparator === "string")) {
        return false;
    }
    for (const property of [
        "code",
        "name",
        "decimalSeparator",
        "dateFormat",
        "timeFormat",
        "formulaArgSeparator",
    ]) {
        if (!locale[property] || typeof locale[property] !== "string") {
            return false;
        }
    }
    if (locale.formulaArgSeparator === locale.decimalSeparator) {
        return false;
    }
    if (locale.thousandsSeparator === locale.decimalSeparator) {
        return false;
    }
    try {
        formatValue(1, { locale, format: "#,##0.00" });
        formatValue(1, { locale, format: locale.dateFormat });
        formatValue(1, { locale, format: locale.timeFormat });
    }
    catch {
        return false;
    }
    return true;
}
/**
 * Change a content string from the given locale to its canonical form (en_US locale). Don't convert date string.
 *
 * @example
 * canonicalizeNumberContent("=SUM(1,5; 02/12/2012)", FR_LOCALE) // "=SUM(1.5, 02/12/2012)"
 * canonicalizeNumberContent("125,9", FR_LOCALE) // "125.9"
 * canonicalizeNumberContent("02/12/2012", FR_LOCALE) // "02/12/2012"
 */
function canonicalizeNumberContent(content, locale) {
    return isFormula(content)
        ? canonicalizeFormula(content, locale)
        : canonicalizeNumberLiteral(content, locale);
}
/**
 * Change a content string from the given locale to its canonical form (en_US locale). Also convert date string.
 * This is destructive and won't preserve the original format.
 *
 * @example
 * canonicalizeContent("=SUM(1,5; 5)", FR_LOCALE) // "=SUM(1.5, 5)"
 * canonicalizeContent("125,9", FR_LOCALE) // "125.9"
 * canonicalizeContent("02/12/2012", FR_LOCALE) // "12/02/2012"
 * canonicalizeContent("02-12-2012", FR_LOCALE) // "12/02/2012"
 */
function canonicalizeContent(content, locale) {
    return isFormula(content)
        ? canonicalizeFormula(content, locale)
        : canonicalizeLiteral(content, locale);
}
/**
 * Change a content string from its canonical form (en_US locale) to the given locale. Also convert date string.
 *
 * @example
 * localizeContent("=SUM(1.5, 5)", FR_LOCALE) // "=SUM(1,5; 5)"
 * localizeContent("125.9", FR_LOCALE) // "125,9"
 * localizeContent("12/02/2012", FR_LOCALE) // "02/12/2012"
 */
function localizeContent(content, locale) {
    return content.startsWith("=")
        ? localizeFormula(content, locale)
        : localizeLiteral(content, locale);
}
/** Change a number string to its canonical form (en_US locale) */
function canonicalizeNumberValue(content, locale) {
    return isFormula(content)
        ? canonicalizeFormula(content, locale)
        : canonicalizeNumberLiteral(content, locale);
}
/** Change a formula to its canonical form (en_US locale) */
function canonicalizeFormula(formula, locale) {
    return _localizeFormula(formula.startsWith("+") ? "=" + formula.slice(1) : formula, locale, DEFAULT_LOCALE);
}
/** Change a formula from the canonical form to the given locale */
function localizeFormula(formula, locale) {
    return _localizeFormula(formula, DEFAULT_LOCALE, locale);
}
function _localizeFormula(formula, fromLocale, toLocale) {
    if (fromLocale.formulaArgSeparator === toLocale.formulaArgSeparator &&
        fromLocale.decimalSeparator === toLocale.decimalSeparator) {
        return formula;
    }
    const tokens = tokenize(formula, fromLocale);
    let localizedFormula = "";
    for (const token of tokens) {
        if (token.type === "NUMBER") {
            localizedFormula += token.value.replace(fromLocale.decimalSeparator, toLocale.decimalSeparator);
        }
        else if (token.type === "ARG_SEPARATOR") {
            localizedFormula += toLocale.formulaArgSeparator;
        }
        else {
            localizedFormula += token.value;
        }
    }
    return localizedFormula;
}
/**
 * Change a literal string from the given locale to its canonical form (en_US locale). Don't convert date string.
 *
 * @example
 * canonicalizeNumberLiteral("125,9", FR_LOCALE) // "125.9"
 * canonicalizeNumberLiteral("02/12/2012", FR_LOCALE) // "02/12/2012"
 */
function canonicalizeNumberLiteral(content, locale) {
    if (locale.decimalSeparator === "." || !isNumber(content, locale)) {
        return content;
    }
    if (locale.thousandsSeparator) {
        content = content.replaceAll(locale.thousandsSeparator, "");
    }
    return content.replace(locale.decimalSeparator, ".");
}
/**
 * Change a content string from the given locale to its canonical form (en_US locale). Also convert date string.
 * This is destructive and won't preserve the original format.
 *
 * @example
 * canonicalizeLiteral("125,9", FR_LOCALE) // "125.9"
 * canonicalizeLiteral("02/12/2012", FR_LOCALE) // "12/02/2012"
 * canonicalizeLiteral("02-12-2012", FR_LOCALE) // "12/02/2012"
 */
function canonicalizeLiteral(content, locale) {
    if (isDateTime(content, locale)) {
        const dateNumber = toNumber(content, locale);
        let format = DEFAULT_LOCALE.dateFormat;
        if (!Number.isInteger(dateNumber)) {
            format += " " + DEFAULT_LOCALE.timeFormat;
        }
        return formatValue(dateNumber, { locale: DEFAULT_LOCALE, format });
    }
    return canonicalizeNumberLiteral(content, locale);
}
/**
 * Change a literal string from its canonical form (en_US locale) to the given locale. Don't convert date string.
 * This is destructive and won't preserve the original format.
 *
 * @example
 * localizeNumberLiteral("125.9", FR_LOCALE) // "125,9"
 * localizeNumberLiteral("12/02/2012", FR_LOCALE) // "12/02/2012"
 * localizeNumberLiteral("12-02-2012", FR_LOCALE) // "12/02/2012"
 */
function localizeNumberLiteral(literal, locale) {
    if (locale.decimalSeparator === "." || !isNumber(literal, DEFAULT_LOCALE)) {
        return literal;
    }
    const decimalNumberRegex = getDecimalNumberRegex(DEFAULT_LOCALE);
    return literal.replace(decimalNumberRegex, (match) => {
        return match.replace(".", locale.decimalSeparator);
    });
}
/**
 * Change a literal string from its canonical form (en_US locale) to the given locale. Also convert date string.
 *
 * @example
 * localizeLiteral("125.9", FR_LOCALE) // "125,9"
 * localizeLiteral("12/02/2012", FR_LOCALE) // "02/12/2012"
 */
function localizeLiteral(literal, locale) {
    if (isDateTime(literal, DEFAULT_LOCALE)) {
        const dateNumber = toNumber(literal, DEFAULT_LOCALE);
        let format = locale.dateFormat;
        if (!Number.isInteger(dateNumber)) {
            format += " " + locale.timeFormat;
        }
        return formatValue(dateNumber, { locale, format });
    }
    return localizeNumberLiteral(literal, locale);
}
function canonicalizeCFRule(cf, locale) {
    return changeCFRuleLocale(cf, (content) => canonicalizeContent(content, locale));
}
function localizeCFRule(cf, locale) {
    return changeCFRuleLocale(cf, (content) => localizeContent(content, locale));
}
function localizeDataValidationRule(rule, locale) {
    const localizedDVRule = deepCopy(rule);
    localizedDVRule.criterion.values = localizedDVRule.criterion.values.map((content) => localizeContent(content, locale));
    return localizedDVRule;
}
function changeCFRuleLocale(rule, changeContentLocale) {
    rule = deepCopy(rule);
    switch (rule.type) {
        case "CellIsRule":
            // Only change value for number operators
            switch (rule.operator) {
                case "isBetween":
                case "isNotBetween":
                case "isEqual":
                case "isNotEqual":
                case "isGreaterThan":
                case "isGreaterOrEqualTo":
                case "isLessThan":
                case "isLessOrEqualTo":
                case "customFormula":
                    rule.values = rule.values.map((v) => changeContentLocale(v));
                    return rule;
                case "beginsWithText":
                case "containsText":
                case "endsWithText":
                case "notContainsText":
                case "isEmpty":
                case "isNotEmpty":
                    return rule;
            }
        case "DataBarRule":
            return rule;
        case "ColorScaleRule":
            rule.minimum = changeCFRuleThresholdLocale(rule.minimum, changeContentLocale);
            rule.maximum = changeCFRuleThresholdLocale(rule.maximum, changeContentLocale);
            if (rule.midpoint) {
                rule.midpoint = changeCFRuleThresholdLocale(rule.midpoint, changeContentLocale);
            }
            return rule;
        case "IconSetRule":
            rule.lowerInflectionPoint.value = changeContentLocale(rule.lowerInflectionPoint.value);
            rule.upperInflectionPoint.value = changeContentLocale(rule.upperInflectionPoint.value);
            return rule;
    }
}
function changeCFRuleThresholdLocale(threshold, changeContentLocale) {
    if (!threshold?.value) {
        return threshold;
    }
    const value = threshold.type === "formula" ? "=" + threshold.value : threshold.value;
    const modified = changeContentLocale(value);
    const newValue = threshold.type === "formula" ? modified.slice(1) : modified;
    return { ...threshold, value: newValue };
}
function getDateTimeFormat(locale) {
    return locale.dateFormat + " " + locale.timeFormat;
}

function boolAnd(args) {
    let foundBoolean = false;
    let acc = true;
    conditionalVisitBoolean(args, (arg) => {
        foundBoolean = true;
        acc = acc && arg;
        return acc;
    });
    return {
        foundBoolean,
        result: acc,
    };
}
function boolOr(args) {
    let foundBoolean = false;
    let acc = false;
    conditionalVisitBoolean(args, (arg) => {
        foundBoolean = true;
        acc = acc || arg;
        return !acc;
    });
    return {
        foundBoolean,
        result: acc,
    };
}

function sum(values, locale) {
    return reduceNumbers(values, (acc, a) => acc + a, 0, locale);
}
function countUnique(args) {
    return reduceAny(args, (acc, a) => (isDataNonEmpty(a) ? acc.add(a?.value) : acc), new Set()).size;
}

function assert(condition, message) {
    if (!condition) {
        throw new EvaluationError(message);
    }
}
function assertNotZero(value, message = _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.")) {
    if (value === 0) {
        throw new DivisionByZeroError(message);
    }
}
function isSingleColOrRow(arg) {
    return arg.length === 1 || arg[0].length === 1;
}
function areSameDimensions(...args) {
    if (args.every(isMatrix)) {
        const cols = args[0].length;
        const rows = args[0][0].length;
        for (const arg of args) {
            if (arg.length !== cols || arg[0].length !== rows) {
                return false;
            }
        }
        return true;
    }
    return !args.some((arg) => Array.isArray(arg) && (arg.length !== 1 || arg[0].length !== 1));
}
function isSquareMatrix(arg) {
    return arg.length === arg[0].length;
}
const expectNumberGreaterThanOrEqualToOne = (value) => _t("The function [[FUNCTION_NAME]] expects a number value to be greater than or equal to 1, but receives %s.", value);

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

function assertSameNumberOfElements(...args) {
    const dims = args[0].length;
    args.forEach((arg, i) => assert(arg.length === dims, _t("[[FUNCTION_NAME]] has mismatched dimensions for argument %s (%s vs %s).", i.toString(), dims.toString(), arg.length.toString())));
}
function average(values, locale) {
    let count = 0;
    const sum = reduceNumbers(values, (acc, a) => {
        count += 1;
        return acc + a;
    }, 0, locale);
    assertNotZero(count);
    return sum / count;
}
function countNumbers(values, locale) {
    let count = 0;
    for (const n of values) {
        if (isMatrix(n)) {
            for (const i of n) {
                for (const j of i) {
                    if (typeof j.value === "number") {
                        count += 1;
                    }
                }
            }
        }
        else {
            const value = n?.value;
            if (!isEvaluationError(value) &&
                (typeof value !== "string" || isNumber(value, locale) || parseDateTime(value, locale))) {
                count += 1;
            }
        }
    }
    return count;
}
function countAny(values) {
    return reduceAny(values, (acc, a) => (a !== undefined && a.value !== null ? acc + 1 : acc), 0);
}
function max(values, locale) {
    let max = { value: -Infinity };
    visitNumbers(values, (a) => {
        if (a.value >= max.value) {
            max = a;
        }
    }, locale);
    return max.value === -Infinity ? { value: 0 } : max;
}
function min(values, locale) {
    let min = { value: Infinity };
    visitNumbers(values, (a) => {
        if (a.value <= min.value) {
            min = a;
        }
    }, locale);
    return min.value === Infinity ? { value: 0 } : min;
}
function prepareDataForRegression(X, Y, newX) {
    const _X = X[0].length ? X : [range(1, Y.flat().length + 1)];
    const nVar = _X.length;
    let _newX = newX[0].length ? newX : _X;
    _newX = _newX.length === nVar ? transposeMatrix(_newX) : _newX;
    return { _X, _newX };
}
/*
 * This function performs a linear regression on the data set. It returns an array with two elements.
 * The first element is the slope, and the second element is the intercept.
 * The linear regression line is: y = slope*x + intercept
 * The function use the least squares method to find the best fit for the data set :
 * see https://www.mathsisfun.com/data/least-squares-regression.html
 *     https://www.statology.org/standard-error-of-estimate/
 *     https://agronomy4future.org/?p=16670
 *     https://vitalflux.com/interpreting-f-statistics-in-linear-regression-formula-examples/
 *     https://web.ist.utl.pt/~ist11038/compute/errtheory/,regression/regrthroughorigin.pdf
 */
function fullLinearRegression(X, Y, computeIntercept = true, verbose = false) {
    const y = Y.flat();
    const n = y.length;
    let { _X } = prepareDataForRegression(X, Y, [[]]);
    _X = _X.length === n ? transposeMatrix(_X) : _X.slice();
    assertSameNumberOfElements(_X[0], y);
    const nVar = _X.length;
    const nDeg = n - nVar - (computeIntercept ? 1 : 0);
    const yMatrix = [y];
    const xMatrix = transposeMatrix(_X.reverse());
    const avgX = [];
    for (let i = 0; i < nVar; i++) {
        avgX.push(0);
        if (computeIntercept) {
            for (const xij of _X[i]) {
                avgX[i] += xij;
            }
            avgX[i] /= n;
        }
    }
    let avgY = 0;
    if (computeIntercept) {
        for (const yi of y) {
            avgY += yi;
        }
        avgY /= n;
    }
    const redX = xMatrix.map((row) => row.map((value, i) => value - avgX[i]));
    if (computeIntercept) {
        xMatrix.forEach((row) => row.push(1));
    }
    const coeffs = getLMSCoefficients(xMatrix, yMatrix);
    if (!computeIntercept) {
        coeffs.push([0]);
    }
    if (!verbose) {
        return coeffs;
    }
    const dot1 = multiplyMatrices(redX, transposeMatrix(redX));
    const { inverted: dotInv } = invertMatrix(dot1);
    if (dotInv === undefined) {
        throw new EvaluationError(_t("Matrix is not invertible"));
    }
    let SSE = 0, SSR = 0;
    for (let i = 0; i < n; i++) {
        const yi = y[i] - avgY;
        let temp = 0;
        for (let j = 0; j < nVar; j++) {
            const xi = redX[i][j];
            temp += xi * coeffs[j][0];
        }
        const ei = yi - temp;
        SSE += ei * ei;
        SSR += temp * temp;
    }
    const RMSE = Math.sqrt(SSE / nDeg);
    const r2 = SSR / (SSR + SSE);
    const f_stat = SSR / nVar / (SSE / nDeg);
    const deltaCoeffs = [];
    for (let i = 0; i < nVar; i++) {
        deltaCoeffs.push(RMSE * Math.sqrt(dotInv[i][i]));
    }
    if (computeIntercept) {
        const dot2 = multiplyMatrices(dotInv, [avgX]);
        const dot3 = multiplyMatrices(transposeMatrix([avgX]), dot2);
        deltaCoeffs.push(RMSE * Math.sqrt(dot3[0][0] + 1 / y.length));
    }
    const returned = [
        [coeffs[0][0], deltaCoeffs[0], r2, f_stat, SSR],
        [coeffs[1][0], deltaCoeffs[1], RMSE, nDeg, SSE],
    ];
    for (let i = 2; i < nVar; i++) {
        returned.push([coeffs[i][0], deltaCoeffs[i], "", "", ""]);
    }
    if (computeIntercept) {
        returned.push([coeffs[nVar][0], deltaCoeffs[nVar], "", "", ""]);
    }
    else {
        returned.push([0, "", "", "", ""]);
    }
    return returned;
}
/*
  This function performs a polynomial regression on the data set. It returns the coefficients of
  the polynomial function that best fits the data set.
  The polynomial function is: y = c0 + c1*x + c2*x^2 + ... + cn*x^n, where n is the order (degree)
  of the polynomial. The returned coefficients are then in the form: [c0, c1, c2, ..., cn]
  The function is based on the method of least squares :
  see: https://mathworld.wolfram.com/LeastSquaresFittingPolynomial.html
*/
function polynomialRegression(flatY, flatX, order, intercept) {
    assertSameNumberOfElements(flatX, flatY);
    assert(order >= 1, _t("Function [[FUNCTION_NAME]] A regression of order less than 1 cannot be possible."));
    const yMatrix = [flatY];
    const xMatrix = flatX.map((x) => range(0, order).map((i) => Math.pow(x, order - i)));
    if (intercept) {
        xMatrix.forEach((row) => row.push(1));
    }
    const coeffs = getLMSCoefficients(xMatrix, yMatrix);
    if (!intercept) {
        coeffs.push([0]);
    }
    return coeffs;
}
function getLMSCoefficients(xMatrix, yMatrix) {
    const xMatrixT = transposeMatrix(xMatrix);
    const dot1 = multiplyMatrices(xMatrix, xMatrixT);
    const { inverted: dotInv } = invertMatrix(dot1);
    if (dotInv === undefined) {
        throw new EvaluationError(_t("Matrix is not invertible"));
    }
    const dot2 = multiplyMatrices(xMatrix, yMatrix);
    return transposeMatrix(multiplyMatrices(dotInv, dot2));
}
function evaluatePolynomial(coeffs, x, order) {
    return coeffs.reduce((acc, coeff, i) => acc + coeff * Math.pow(x, order - i), 0);
}
function expM(M) {
    return M.map((col) => col.map((cell) => Math.exp(cell)));
}
function logM(M) {
    return M.map((col) => col.map((cell) => Math.log(cell)));
}
function predictLinearValues(Y, X, newX, computeIntercept) {
    const { _X, _newX } = prepareDataForRegression(X, Y, newX);
    const coeffs = fullLinearRegression(_X, Y, computeIntercept, false);
    const nVar = coeffs.length - 1;
    const newY = _newX.map((col) => {
        let value = 0;
        for (let i = 0; i < nVar; i++) {
            value += coeffs[i][0] * col[nVar - i - 1];
        }
        value += coeffs[nVar][0];
        return [value];
    });
    return newY.length === newX.length ? newY : transposeMatrix(newY);
}
function getMovingAverageValues(dataset, labels, windowSize = DEFAULT_WINDOW_SIZE) {
    const values = [];
    // Fill the starting values with null until we have a full window
    for (let i = 0; i < windowSize - 1; i++) {
        values.push({ x: labels[i], y: NaN });
    }
    for (let i = 0; i <= dataset.length - windowSize; i++) {
        let sum = 0;
        for (let j = i; j < i + windowSize; j++) {
            sum += dataset[j];
        }
        values.push({ x: labels[i + windowSize - 1], y: sum / windowSize });
    }
    return values;
}

const pivotTimeAdapterRegistry = new Registry();
function pivotTimeAdapter(granularity) {
    return pivotTimeAdapterRegistry.get(granularity);
}
/**
 * The Time Adapter: Managing Time Periods for Pivot Functions
 *
 * Overview:
 * A time adapter is responsible for managing time periods associated with pivot functions.
 * Each type of period (day, week, month, quarter, etc.) has its own dedicated adapter.
 * The adapter's primary role is to normalize period values between spreadsheet functions,
 * and the pivot.
 * By normalizing the period value, it can be stored consistently in the pivot.
 *
 * Normalization Process:
 * When working with functions in the spreadsheet, the time adapter normalizes
 * the provided period to facilitate accurate lookup of values in the pivot.
 * For instance, if the spreadsheet function represents a day period as a number generated
 * by the DATE function (DATE(2023, 12, 25)), the time adapter will normalize it accordingly.
 *
 */
/**
 * Normalized value: "12/25/2023"
 *
 * Note: Those two format are equivalent:
 * - "MM/dd/yyyy" (luxon format)
 * - "mm/dd/yyyy" (spreadsheet format)
 **/
const dayAdapter = {
    normalizeFunctionValue(value) {
        return toNumber(value, DEFAULT_LOCALE);
    },
    toValueAndFormat(normalizedValue, locale) {
        return {
            value: toNumber(normalizedValue, DEFAULT_LOCALE),
            format: "dd mmm yyyy",
        };
    },
    toFunctionValue(normalizedValue) {
        const jsDate = toJsDate(normalizedValue, DEFAULT_LOCALE);
        return `DATE(${jsDate.getFullYear()},${jsDate.getMonth() + 1},${jsDate.getDate()})`;
    },
};
/**
 * normalizes day of month number
 */
const dayOfMonthAdapter = {
    normalizeFunctionValue(value) {
        const day = toNumber(value, DEFAULT_LOCALE);
        if (day < 1 || day > 31) {
            throw new EvaluationError(_t("%s is not a valid day of month (it should be a number between 1 and 31)", day));
        }
        return day;
    },
    toValueAndFormat(normalizedValue) {
        return {
            value: toNumber(normalizedValue, DEFAULT_LOCALE),
            format: "0",
        };
    },
    toFunctionValue(normalizedValue) {
        return `${normalizedValue}`;
    },
};
/**
 * normalizes day of week number
 *
 * The day of week is a bit special as it depends on the locale week start day.
 * =PIVOT.VALUE(1, "xx:day_of_week", 1) will be different depending on the locale
 *  - fr_FR: 1: Monday, 7: Sunday   (weekStart = 1)
 *  - en_US: 1: Sunday, 7: Saturday (weekStart = 7)
 *
 * The function that normalizes the value coming from the function
 * (`normalizeFunctionValue`) will return the day of week (1 based index)
 * depending on the locale week start day.
 * To display the value in the pivot, we need to convert it to retrieve the
 * correct day of week name (1 should be "Monday" in fr_FR and "Sunday" in en_US).
 */
const dayOfWeekAdapter = {
    normalizeFunctionValue(value) {
        const day = toNumber(value, DEFAULT_LOCALE);
        if (day < 1 || day > 7) {
            throw new EvaluationError(_t("%s is not a valid day of week (it should be a number between 1 and 7)", day));
        }
        return day;
    },
    toValueAndFormat(normalizedValue, locale) {
        /**
         * As explain above, normalizedValue is the day of week (1 based index)
         * depending on the locale week start day. To retrieve the correct day name,
         * we need to convert it to a 0 based index with 0 being Sunday. (DAYS is
         * an object of day names with 0 being Sunday)
         */
        const index = (normalizedValue - 1 + (locale || DEFAULT_LOCALE).weekStart) % 7;
        return {
            value: DAYS$1[index].toString(),
            format: "@",
        };
    },
    toFunctionValue(normalizedValue) {
        return `${normalizedValue}`;
    },
};
/**
 * normalizes iso week number
 */
const isoWeekNumberAdapter = {
    normalizeFunctionValue(value) {
        const isoWeek = toNumber(value, DEFAULT_LOCALE);
        if (isoWeek < 0 || isoWeek > 53) {
            throw new EvaluationError(_t("%s is not a valid week (it should be a number between 0 and 53)", isoWeek));
        }
        return isoWeek;
    },
    toValueAndFormat(normalizedValue) {
        return {
            value: toNumber(normalizedValue, DEFAULT_LOCALE),
            format: "0",
        };
    },
    toFunctionValue(normalizedValue) {
        return `${normalizedValue}`;
    },
};
/**
 * normalizes month number
 */
const monthNumberAdapter = {
    normalizeFunctionValue(value) {
        const month = toNumber(value, DEFAULT_LOCALE);
        if (month < 1 || month > 12) {
            throw new EvaluationError(_t("%s is not a valid month (it should be a number between 1 and 12)", month));
        }
        return month;
    },
    toValueAndFormat(normalizedValue) {
        return {
            value: MONTHS[toNumber(normalizedValue, DEFAULT_LOCALE) - 1].toString(),
            format: "@",
        };
    },
    toFunctionValue(normalizedValue) {
        return `${normalizedValue}`;
    },
};
/**
 * normalizes month number + year
 */
const monthAdapter = {
    normalizeFunctionValue(value) {
        const date = toNumber(value, DEFAULT_LOCALE);
        return formatValue(date, { locale: DEFAULT_LOCALE, format: "mm/yyyy" });
    },
    toValueAndFormat(normalizedValue) {
        return {
            value: toNumber(normalizedValue, DEFAULT_LOCALE),
            format: "mmmm yyyy",
        };
    },
    toFunctionValue(normalizedValue) {
        const jsDate = toJsDate(normalizedValue, DEFAULT_LOCALE);
        return `DATE(${jsDate.getFullYear()},${jsDate.getMonth() + 1},1)`;
    },
};
/**
 * normalizes quarter number
 */
const quarterNumberAdapter = {
    normalizeFunctionValue(value) {
        const quarter = toNumber(value, DEFAULT_LOCALE);
        if (quarter < 1 || quarter > 4) {
            throw new EvaluationError(_t("%s is not a valid quarter (it should be a number between 1 and 4)", quarter));
        }
        return quarter;
    },
    toValueAndFormat(normalizedValue) {
        return {
            value: _t("Q%(quarter_number)s", { quarter_number: normalizedValue }),
            format: "@",
        };
    },
    toFunctionValue(normalizedValue) {
        return `${normalizedValue}`;
    },
};
const yearAdapter = {
    normalizeFunctionValue(value) {
        const year = toNumber(value, DEFAULT_LOCALE);
        if (year > 3000) {
            // interpret the value as a full date
            return toJsDate(year, DEFAULT_LOCALE).getFullYear();
        }
        return year;
    },
    toValueAndFormat(normalizedValue) {
        return {
            value: toNumber(normalizedValue, DEFAULT_LOCALE),
            format: "0",
        };
    },
    toFunctionValue(normalizedValue) {
        return `${normalizedValue}`;
    },
};
/**
 * normalizes hour number
 */
const hourNumberAdapter = {
    normalizeFunctionValue(value) {
        const hour = toNumber(value, DEFAULT_LOCALE);
        if (hour < 0 || hour > 23) {
            throw new EvaluationError(_t("%s is not a valid hour (it should be a number between 0 and 23)", hour));
        }
        return hour;
    },
    toValueAndFormat(normalizedValue) {
        return {
            value: _t("%(hour_number)sh", { hour_number: normalizedValue }),
            format: "@",
        };
    },
    toFunctionValue(normalizedValue) {
        return `${normalizedValue}`;
    },
};
/**
 * normalizes hour number
 */
const minuteNumberAdapter = {
    normalizeFunctionValue(value) {
        const minute = toNumber(value, DEFAULT_LOCALE);
        if (minute < 0 || minute > 59) {
            throw new EvaluationError(_t("%s is not a valid minute (it should be a number between 0 and 59)", minute));
        }
        return minute;
    },
    toValueAndFormat(normalizedValue) {
        return {
            value: _t("%(minute_number)s'", { minute_number: normalizedValue }),
            format: "@",
        };
    },
    toFunctionValue(normalizedValue) {
        return `${normalizedValue}`;
    },
};
/**
 * normalizes second number
 */
const secondNumberAdapter = {
    normalizeFunctionValue(value) {
        const second = toNumber(value, DEFAULT_LOCALE);
        if (second < 0 || second > 59) {
            throw new EvaluationError(_t("%s is not a valid second (it should be a number between 0 and 59)", second));
        }
        return second;
    },
    toValueAndFormat(normalizedValue) {
        return {
            value: _t("%(second_number)s''", { second_number: normalizedValue }),
            format: "@",
        };
    },
    toFunctionValue(normalizedValue) {
        return `${normalizedValue}`;
    },
};
/**
 * This function takes an adapter and wraps it with a null handler.
 * null value means that the value is not set.
 */
function nullHandlerDecorator(adapter) {
    return {
        normalizeFunctionValue(value) {
            if (value === null) {
                return null;
            }
            return adapter.normalizeFunctionValue(value);
        },
        toValueAndFormat(normalizedValue, locale) {
            if (normalizedValue === null) {
                return { value: _t("(Undefined)") }; //TODO Return NA ?
            }
            return adapter.toValueAndFormat(normalizedValue, locale);
        },
        toFunctionValue(normalizedValue) {
            if (normalizedValue === null) {
                return "false"; //TODO Return NA ?
            }
            return adapter.toFunctionValue(normalizedValue);
        },
    };
}
pivotTimeAdapterRegistry
    .add("day", nullHandlerDecorator(dayAdapter))
    .add("year", nullHandlerDecorator(yearAdapter))
    .add("day_of_month", nullHandlerDecorator(dayOfMonthAdapter))
    .add("iso_week_number", nullHandlerDecorator(isoWeekNumberAdapter))
    .add("month_number", nullHandlerDecorator(monthNumberAdapter))
    .add("month", nullHandlerDecorator(monthAdapter))
    .add("quarter_number", nullHandlerDecorator(quarterNumberAdapter))
    .add("day_of_week", nullHandlerDecorator(dayOfWeekAdapter))
    .add("hour_number", nullHandlerDecorator(hourNumberAdapter))
    .add("minute_number", nullHandlerDecorator(minuteNumberAdapter))
    .add("second_number", nullHandlerDecorator(secondNumberAdapter));

const AGGREGATOR_NAMES = {
    count: _t("Count"),
    count_distinct: _t("Count Distinct"),
    bool_and: _t("Boolean And"),
    bool_or: _t("Boolean Or"),
    max: _t("Maximum"),
    min: _t("Minimum"),
    avg: _t("Average"),
    sum: _t("Sum"),
};
const DEFAULT_AGGREGATORS = ["max", "min", "avg", "sum", "count_distinct", "count"];
const AGGREGATORS_BY_FIELD_TYPE = {
    integer: DEFAULT_AGGREGATORS,
    char: DEFAULT_AGGREGATORS,
    datetime: DEFAULT_AGGREGATORS,
    boolean: ["count_distinct", "count", "bool_and", "bool_or"],
};
const AGGREGATORS = {};
for (const type in AGGREGATORS_BY_FIELD_TYPE) {
    AGGREGATORS[type] = {};
    for (const aggregator of AGGREGATORS_BY_FIELD_TYPE[type]) {
        AGGREGATORS[type][aggregator] = AGGREGATOR_NAMES[aggregator];
    }
}
const AGGREGATORS_FN = {
    count: (args) => ({
        value: countAny([args]),
        format: "0",
    }),
    count_distinct: (args) => ({
        value: countUnique([args]),
        format: "0",
    }),
    bool_and: (args) => ({
        value: boolAnd([args]).result,
    }),
    bool_or: (args) => ({
        value: boolOr([args]).result,
    }),
    max: (args, locale) => max([args], locale),
    min: (args, locale) => min([args], locale),
    avg: (args, locale) => ({
        value: average([args], locale),
        format: inferFormat(args),
    }),
    sum: (args, locale) => ({
        value: sum([args], locale),
        format: inferFormat(args),
    }),
};
/**
 * Given an object of form {"1": {...}, "2": {...}, ...} get the maximum ID used
 * in this object
 * If the object has no keys, return 0
 *
 */
function getMaxObjectId(o) {
    const keys = Object.keys(o);
    if (!keys.length) {
        return 0;
    }
    const nums = keys.map((id) => parseInt(id, 10));
    return Math.max(...nums);
}
const ALL_PERIODS = {
    year: _t("Year"),
    quarter: _t("Quarter & Year"),
    month: _t("Month & Year"),
    week: _t("Week & Year"),
    day: _t("Day"),
    quarter_number: _t("Quarter"),
    month_number: _t("Month"),
    iso_week_number: _t("Week"),
    day_of_month: _t("Day of Month"),
    day_of_week: _t("Day of Week"),
    hour_number: _t("Hour"),
    minute_number: _t("Minute"),
    second_number: _t("Second"),
};
const DATE_FIELDS = ["date", "datetime"];
/**
 * Parse a dimension string into a pivot dimension definition.
 * e.g "create_date:month" => { name: "create_date", granularity: "month" }
 */
function parseDimension(dimension) {
    const [fieldName, granularity] = dimension.split(":");
    if (granularity) {
        return { fieldName, granularity };
    }
    return { fieldName };
}
function isDateOrDatetimeField(field) {
    return DATE_FIELDS.includes(field.type);
}
function generatePivotArgs(formulaId, domain, measure) {
    const args = [formulaId];
    if (measure) {
        args.push(`"${measure}"`);
    }
    for (const { field, value, type } of domain) {
        if (field === "measure") {
            args.push(`"measure"`, `"${value}"`);
            continue;
        }
        const { granularity } = parseDimension(field);
        const formattedValue = toFunctionPivotValue(value, { type, granularity });
        args.push(`"${field}"`, formattedValue);
    }
    return args;
}
/**
 * Check if the fields in the domain part of
 * a pivot function are valid according to the pivot definition.
 * e.g. =PIVOT.VALUE(1,"revenue","country_id",...,"create_date:month",...,"source_id",...)
 */
function areDomainArgsFieldsValid(dimensions, definition) {
    let argIndex = 0;
    let definitionIndex = 0;
    const cols = definition.columns.map((col) => col.nameWithGranularity);
    const rows = definition.rows.map((row) => row.nameWithGranularity);
    while (dimensions[argIndex] !== undefined && dimensions[argIndex] === rows[definitionIndex]) {
        argIndex++;
        definitionIndex++;
    }
    definitionIndex = 0;
    while (dimensions[argIndex] !== undefined && dimensions[argIndex] === cols[definitionIndex]) {
        argIndex++;
        definitionIndex++;
    }
    return dimensions.length === argIndex;
}
function createPivotFormula(formulaId, cell) {
    switch (cell.type) {
        case "HEADER":
            return `=PIVOT.HEADER(${generatePivotArgs(formulaId, cell.domain).join(",")})`;
        case "VALUE":
            return `=PIVOT.VALUE(${generatePivotArgs(formulaId, cell.domain, cell.measure).join(",")})`;
        case "MEASURE_HEADER":
            return `=PIVOT.HEADER(${generatePivotArgs(formulaId, [
                ...cell.domain,
                { field: "measure", value: cell.measure, type: "char" },
            ]).join(",")})`;
    }
    return "";
}
/**
 * Parses the value defining a pivot group in a PIVOT formula
 * e.g. given the following formula PIVOT.VALUE("1", "stage_id", "42", "status", "won"),
 * the two group values are "42" and "won".
 */
function toNormalizedPivotValue(dimension, groupValue) {
    if (groupValue === null || groupValue === "null") {
        return null;
    }
    const extractedGroupValue = typeof groupValue === "object" ? groupValue.value : groupValue;
    if (isEvaluationError(extractedGroupValue)) {
        return extractedGroupValue;
    }
    if (dimension.type === "custom") {
        return toValue(groupValue) ?? null;
    }
    const groupValueString = typeof groupValue === "boolean"
        ? toString(groupValue).toLocaleLowerCase()
        : toString(groupValue);
    if (groupValueString === "null") {
        return null;
    }
    if (!pivotNormalizationValueRegistry.contains(dimension.type)) {
        throw new EvaluationError(_t("Field %(field)s is not supported because of its type (%(type)s)", {
            field: dimension.displayName,
            type: dimension.type,
        }));
    }
    // represents a field which is not set (=False server side)
    if (groupValueString.toLowerCase() === "false") {
        return false;
    }
    const normalizer = pivotNormalizationValueRegistry.get(dimension.type);
    return normalizer(groupValueString, dimension.granularity);
}
function normalizeDateTime(value, granularity) {
    return pivotTimeAdapter(granularity ?? "month").normalizeFunctionValue(value);
}
function toFunctionPivotValue(value, dimension) {
    if (value === null) {
        return `"null"`;
    }
    if (!pivotToFunctionValueRegistry.contains(dimension.type)) {
        return `"${value}"`;
    }
    return pivotToFunctionValueRegistry.get(dimension.type)(value, dimension.granularity);
}
function toFunctionValueDateTime(value, granularity) {
    return pivotTimeAdapter(granularity ?? "month").toFunctionValue(value);
}
const pivotNormalizationValueRegistry = new Registry();
pivotNormalizationValueRegistry
    .add("date", normalizeDateTime)
    .add("datetime", normalizeDateTime)
    .add("integer", (value) => toNumber(value, DEFAULT_LOCALE))
    .add("boolean", (value) => toBoolean(value))
    .add("char", (value) => toString(value))
    .add("custom", (value) => value);
const pivotToFunctionValueRegistry = new Registry();
pivotToFunctionValueRegistry
    .add("date", toFunctionValueDateTime)
    .add("datetime", toFunctionValueDateTime)
    .add("integer", (value) => `${toNumber(value, DEFAULT_LOCALE)}`)
    .add("boolean", (value) => (toBoolean(value) ? "TRUE" : "FALSE"))
    .add("char", (value) => `"${toString(value).replace(/"/g, '\\"')}"`)
    .add("custom", (value) => (typeof value === "string" ? `"${value}"` : String(value)));
function getFieldDisplayName(field) {
    return field.displayName + (field.granularity ? ` (${ALL_PERIODS[field.granularity]})` : "");
}
function addAlignFormatToPivotHeader(domain, functionResult) {
    if (domain.length === 0) {
        return functionResult;
    }
    return { ...functionResult, format: (functionResult.format || "@") + "* " };
}
function isSortedColumnValid(sortedColumn, pivot) {
    try {
        if (!pivot.getMeasure(sortedColumn.measure)) {
            return false;
        }
        const columns = pivot.definition.columns;
        for (let i = 0; i < sortedColumn.domain.length; i++) {
            if (columns[i].nameWithGranularity !== sortedColumn.domain[i].field) {
                return false;
            }
            const possibleValues = pivot
                .getPossibleFieldValues(columns[i])
                .map((v) => v.value);
            if (!possibleValues.includes(sortedColumn.domain[i].value) &&
                !(sortedColumn.domain[i].value === null && possibleValues.includes(""))) {
                return false;
            }
        }
        return true;
    }
    catch (e) {
        return false;
    }
}
function getUniquePivotGroupName(baseName, field) {
    const groupNames = field.groups.map((g) => g.name);
    return getUniqueText(baseName, groupNames, {
        compute: (name, i) => `${name}${i}`,
        start: 2,
    });
}
function getUniquePivotFieldName(baseName, fields) {
    const namesToAvoid = Object.values(fields)
        .map((f) => [f?.name, f?.string])
        .flat()
        .filter(isDefined);
    return getUniqueText(baseName, namesToAvoid, {
        compute: (name, i) => `${name}${i}`,
        start: 2,
    });
}
function createCustomFields(definition, fields) {
    const newFields = {};
    for (const customField of Object.values(definition.customFields || {})) {
        const parentField = fields[customField.parentField];
        if (!parentField) {
            continue;
        }
        newFields[customField.name] = {
            type: "custom",
            isCustomField: true,
            name: customField.name,
            string: customField.name,
            customGroups: customField.groups,
            parentField: customField.parentField,
        };
    }
    return newFields;
}
function removePivotGroupsContainingValues(valuesToRemove, customField) {
    customField.groups = customField.groups.filter((group) => !group.values.some((value) => valuesToRemove.includes(value)));
}
/**
 * Adds a new dimension to the pivot definition before a specified base dimension.
 * If the new dimension already exists, it does nothing.
 */
function addDimensionToPivotDefinition(definition, baseDimension, newDimension) {
    const dimensions = definition.rows.some((dim) => dim.fieldName === baseDimension)
        ? definition.rows
        : definition.columns;
    const baseIndex = dimensions.findIndex((dim) => dim.fieldName === baseDimension);
    if (baseIndex === -1) {
        return definition;
    }
    if (dimensions.some((dim) => dim.fieldName === newDimension)) {
        return definition;
    }
    dimensions.splice(baseIndex, 0, { fieldName: newDimension });
    return definition;
}
function getCustomFieldWithParentField(definition, parentField, fields) {
    return (Object.values(definition.customFields || {}).find((field) => field.parentField === parentField.name) || {
        parentField: parentField.name,
        name: getUniquePivotFieldName(parentField.string, fields),
        groups: [],
    });
}
/**
 * Collapse a hierarchical display name by keeping only the first and last parts.
 * Example: "Sales > Europe > France" → "Sales > … > France"
 */
function collapseHierarchicalDisplayName(displayName, splitter = ">") {
    const firstIndex = displayName.indexOf(splitter);
    const lastIndex = displayName.lastIndexOf(splitter);
    if (!displayName || !firstIndex || firstIndex === lastIndex) {
        return displayName;
    }
    const firstPart = displayName.slice(0, firstIndex + 1);
    const lastPart = displayName.slice(lastIndex);
    return `${firstPart} … ${lastPart}`;
}

class CellClipboardHandler extends AbstractCellClipboardHandler {
    isCutAllowed(data) {
        if (data.zones.length !== 1) {
            return "WrongCutSelection" /* CommandResult.WrongCutSelection */;
        }
        return "Success" /* CommandResult.Success */;
    }
    copy(data, isCutOperation, mode = "copyPaste") {
        const sheetId = data.sheetId;
        const { clippedZones, rowsIndexes, columnsIndexes } = data;
        const clippedCells = [];
        const isCopyingOneCell = rowsIndexes.length === 1 && columnsIndexes.length === 1;
        for (const row of rowsIndexes) {
            const cellsInRow = [];
            for (const col of columnsIndexes) {
                const position = { col, row, sheetId };
                let cell = this.getters.getCell(position);
                const evaluatedCell = this.getters.getEvaluatedCell(position);
                const pivotId = this.getters.getPivotIdFromPosition(position);
                const spreader = this.getters.getArrayFormulaSpreadingOn(position);
                if (mode !== "shiftCells" && pivotId && spreader) {
                    const pivotZone = this.getters.getSpreadZone(spreader);
                    if ((!deepEquals(spreader, position) || !isCopyingOneCell) &&
                        pivotZone &&
                        !data.zones.some((z) => isZoneInside(pivotZone, z))) {
                        const pivotCell = this.getters.getPivotCellFromPosition(position);
                        const formulaPivotId = this.getters.getPivotFormulaId(pivotId);
                        const pivotFormula = createPivotFormula(formulaPivotId, pivotCell);
                        cell = {
                            id: cell?.id || "",
                            style: cell?.style,
                            format: cell?.format,
                            content: pivotFormula,
                            isFormula: false,
                            parsedValue: evaluatedCell.value,
                        };
                    }
                }
                else if (mode !== "shiftCells") {
                    if (spreader && !deepEquals(spreader, position)) {
                        const isSpreaderCopied = rowsIndexes.includes(spreader.row) && columnsIndexes.includes(spreader.col);
                        const content = isSpreaderCopied
                            ? ""
                            : formatValue(evaluatedCell.value, { locale: this.getters.getLocale() });
                        cell = {
                            id: cell?.id || "",
                            style: cell?.style,
                            format: evaluatedCell.format,
                            content,
                            isFormula: false,
                            parsedValue: evaluatedCell.value,
                        };
                    }
                }
                cellsInRow.push({
                    content: cell?.content ?? "",
                    style: cell?.style,
                    format: cell?.format,
                    tokens: cell?.isFormula
                        ? cell.compiledFormula.tokens.map(({ value, type }) => ({ value, type }))
                        : [],
                    border: this.getters.getCellBorder(position) || undefined,
                    evaluatedCell,
                    position,
                });
            }
            clippedCells.push(cellsInRow);
        }
        return {
            cells: clippedCells,
            zones: clippedZones,
            sheetId: data.sheetId,
        };
    }
    isPasteAllowed(sheetId, target, content, clipboardOptions) {
        if (!content.cells) {
            return "Success" /* CommandResult.Success */;
        }
        if (clipboardOptions?.isCutOperation && clipboardOptions?.pasteOption !== undefined) {
            // cannot paste only format or only value if the previous operation is a CUT
            return "WrongPasteOption" /* CommandResult.WrongPasteOption */;
        }
        if (target.length > 1) {
            // cannot paste if we have a clipped zone larger than a cell and multiple
            // zones selected
            if (content.cells.length > 1 || content.cells[0].length > 1) {
                return "WrongPasteSelection" /* CommandResult.WrongPasteSelection */;
            }
        }
        const clipboardHeight = content.cells.length;
        const clipboardWidth = content.cells[0].length;
        for (const zone of getPasteZones(target, content.cells)) {
            if (this.getters.doesIntersectMerge(sheetId, zone)) {
                if (target.length > 1 ||
                    !this.getters.isSingleCellOrMerge(sheetId, target[0]) ||
                    clipboardHeight * clipboardWidth !== 1) {
                    return "WillRemoveExistingMerge" /* CommandResult.WillRemoveExistingMerge */;
                }
            }
        }
        return "Success" /* CommandResult.Success */;
    }
    /**
     * Paste the clipboard content in the given target
     */
    paste(target, content, options) {
        const zones = target.zones;
        const sheetId = target.sheetId;
        if (!options.isCutOperation) {
            this.pasteFromCopy(sheetId, zones, content.cells, options);
        }
        else {
            this.pasteFromCut(sheetId, zones, content, options);
        }
    }
    getPasteTarget(sheetId, target, content, options) {
        const width = content.cells[0].length;
        const height = content.cells.length;
        if (options?.isCutOperation) {
            return {
                sheetId,
                zones: [
                    {
                        left: target[0].left,
                        top: target[0].top,
                        right: target[0].left + width - 1,
                        bottom: target[0].top + height - 1,
                    },
                ],
            };
        }
        if (width === 1 && height === 1) {
            return { zones: [], sheetId };
        }
        return { sheetId, zones: getPasteZones(target, content.cells) };
    }
    pasteFromCut(sheetId, target, content, options) {
        this.clearClippedZones(content);
        const selection = target[0];
        this.pasteZone(sheetId, selection.left, selection.top, content.cells, options);
    }
    /**
     * Clear the clipped zones: remove the cells and clear the formatting
     */
    clearClippedZones(content) {
        this.dispatch("CLEAR_CELLS", {
            sheetId: content.sheetId,
            target: content.zones,
        });
        this.dispatch("CLEAR_FORMATTING", {
            sheetId: content.sheetId,
            target: content.zones,
        });
    }
    pasteZone(sheetId, col, row, cells, clipboardOptions) {
        // then, perform the actual paste operation
        for (const [r, rowCells] of cells.entries()) {
            for (const [c, origin] of rowCells.entries()) {
                if (!origin) {
                    continue;
                }
                const position = { col: col + c, row: row + r, sheetId };
                this.pasteCell(origin, position, clipboardOptions);
            }
        }
    }
    /**
     * Paste the cell at the given position to the target position
     */
    pasteCell(origin, target, clipboardOption) {
        const { sheetId, col, row } = target;
        const targetCell = this.getters.getEvaluatedCell(target);
        const originFormat = origin?.format || origin.evaluatedCell.format;
        if (clipboardOption?.pasteOption === "asValue") {
            this.dispatch("UPDATE_CELL", {
                ...target,
                content: origin.evaluatedCell.value?.toString() || "",
                format: originFormat,
            });
            return;
        }
        if (clipboardOption?.pasteOption === "onlyFormat") {
            this.dispatch("UPDATE_CELL", {
                ...target,
                style: origin?.style ?? null,
                format: originFormat ?? targetCell.format,
            });
            return;
        }
        let content = origin?.content;
        if (origin?.tokens && origin.tokens.length > 0 && !clipboardOption?.isCutOperation) {
            content = this.getters.getTranslatedCellFormula(sheetId, col - origin.position.col, row - origin.position.row, origin.tokens);
        }
        else if (origin?.tokens && origin.tokens.length > 0) {
            content = this.getters.getFormulaMovedInSheet(origin.position.sheetId, sheetId, origin.tokens);
        }
        if (content !== "" || origin?.format || origin?.style) {
            this.dispatch("UPDATE_CELL", {
                ...target,
                content,
                style: origin?.style || null,
                format: origin?.format,
            });
        }
        else if (targetCell) {
            this.dispatch("CLEAR_CELL", target);
        }
    }
    convertTextToClipboardData(text) {
        const locale = this.getters.getLocale();
        const copiedData = {
            cells: [],
        };
        const values = [];
        let rowLength = 0;
        for (const [i, row] of text.replace(/\r/g, "").split("\n").entries()) {
            values.push(row.split("\t"));
            if (values[i].length > rowLength) {
                rowLength = values[i].length;
            }
        }
        for (const row of values) {
            const cells = [];
            for (let i = 0; i < rowLength; i++) {
                const content = canonicalizeNumberValue(row[i] || "", locale);
                cells.push({
                    content: content,
                    evaluatedCell: {
                        formattedValue: content,
                    },
                });
            }
            copiedData.cells.push(cells);
        }
        return copiedData;
    }
}

class ChartClipboardHandler extends AbstractFigureClipboardHandler {
    copy(data) {
        const sheetId = data.sheetId;
        const figure = this.getters.getFigure(sheetId, data.figureId);
        if (!figure) {
            throw new Error(`No figure for the given id: ${data.figureId}`);
        }
        if (figure.tag !== "chart") {
            return;
        }
        const copiedFigure = { ...figure };
        const chart = this.getters.getChartFromFigureId(data.figureId);
        if (!chart) {
            throw new Error(`No chart for the given id: ${data.figureId}`);
        }
        const copiedChart = chart.copyInSheetId(sheetId);
        return {
            figureId: data.figureId,
            copiedFigure,
            copiedChart,
        };
    }
    getPasteTarget(sheetId, target, content, options) {
        const newId = new UuidGenerator().smallUuid();
        return { zones: [], figureId: newId, sheetId };
    }
    paste(target, clippedContent, options) {
        if (!target.figureId) {
            return;
        }
        const { zones, figureId } = target;
        const sheetId = target.sheetId;
        const { width, height } = clippedContent.copiedFigure;
        const copy = clippedContent.copiedChart.copyInSheetId(sheetId);
        const maxPosition = this.getters.getMaxAnchorOffset(sheetId, height, width);
        let { left: col, top: row } = zones[0];
        const offset = { x: 0, y: 0 };
        if (col > maxPosition.col) {
            col = maxPosition.col;
            offset.x = maxPosition.offset.x;
        }
        if (row > maxPosition.row) {
            row = maxPosition.row;
            offset.y = maxPosition.offset.y;
        }
        this.dispatch("CREATE_CHART", {
            figureId,
            chartId: new UuidGenerator().smallUuid(),
            sheetId,
            definition: copy.getDefinition(),
            col,
            row,
            offset,
            size: { height, width },
        });
        if (options.isCutOperation) {
            this.dispatch("DELETE_FIGURE", {
                sheetId: clippedContent.copiedChart.sheetId,
                figureId: clippedContent.copiedFigure.id,
            });
        }
        this.dispatch("SELECT_FIGURE", { figureId });
    }
    isPasteAllowed(sheetId, target, content, option) {
        if (target.length === 0) {
            return "EmptyTarget" /* CommandResult.EmptyTarget */;
        }
        if (option?.pasteOption !== undefined) {
            return "WrongFigurePasteOption" /* CommandResult.WrongFigurePasteOption */;
        }
        return "Success" /* CommandResult.Success */;
    }
}

class ConditionalFormatClipboardHandler extends AbstractCellClipboardHandler {
    uuidGenerator = new UuidGenerator();
    queuedChanges = {};
    copy(data) {
        if (!data.zones.length) {
            return;
        }
        const { rowsIndexes, columnsIndexes } = data;
        const sheetId = data.sheetId;
        const cfRules = [];
        for (const row of rowsIndexes) {
            const cfRuleInRow = [];
            for (const col of columnsIndexes) {
                const cfRules = Array.from(this.getters.getRulesByCell(sheetId, col, row));
                cfRuleInRow.push({
                    position: { col, row, sheetId },
                    rules: cfRules,
                });
            }
            cfRules.push(cfRuleInRow);
        }
        return { cfRules };
    }
    paste(target, clippedContent, options) {
        this.queuedChanges = {};
        if (options.pasteOption === "asValue") {
            return;
        }
        const zones = target.zones;
        const sheetId = target.sheetId;
        if (!options.isCutOperation) {
            this.pasteFromCopy(sheetId, zones, clippedContent.cfRules, options);
        }
        else {
            this.pasteFromCut(sheetId, zones, clippedContent);
        }
        this.executeQueuedChanges();
    }
    pasteFromCut(sheetId, target, content) {
        const selection = target[0];
        this.pasteZone(sheetId, selection.left, selection.top, content.cfRules, {
            isCutOperation: true,
        });
    }
    pasteZone(sheetId, col, row, cfRules, clipboardOptions) {
        for (const [r, rowCells] of cfRules.entries()) {
            for (const [c, origin] of rowCells.entries()) {
                const position = { col: col + c, row: row + r, sheetId };
                this.pasteCf(origin, position, clipboardOptions?.isCutOperation);
            }
        }
    }
    pasteCf(origin, target, isCutOperation) {
        if (origin?.rules && origin.rules.length > 0) {
            const originZone = positionToZone(origin.position);
            const zone = positionToZone(target);
            for (const rule of origin.rules) {
                const toRemoveZones = [];
                if (isCutOperation) {
                    //remove from current rule
                    toRemoveZones.push(originZone);
                }
                if (origin.position.sheetId === target.sheetId) {
                    this.adaptCFRules(origin.position.sheetId, rule, [zone], toRemoveZones);
                }
                else {
                    this.adaptCFRules(origin.position.sheetId, rule, [], toRemoveZones);
                    const cfToCopyTo = this.getCFToCopyTo(target.sheetId, rule);
                    this.adaptCFRules(target.sheetId, cfToCopyTo, [zone], []);
                }
            }
        }
    }
    /**
     * Add or remove cells to a given conditional formatting rule.
     */
    adaptCFRules(sheetId, cf, toAdd, toRemove) {
        if (!this.queuedChanges[sheetId]) {
            this.queuedChanges[sheetId] = [];
        }
        const queuedChange = this.queuedChanges[sheetId].find((queued) => queued.cf.id === cf.id);
        if (!queuedChange) {
            this.queuedChanges[sheetId].push({ toAdd, toRemove, cf });
        }
        else {
            queuedChange.toAdd.push(...toAdd);
            queuedChange.toRemove.push(...toRemove);
        }
    }
    executeQueuedChanges() {
        for (const sheetId in this.queuedChanges) {
            for (const { toAdd, toRemove, cf } of this.queuedChanges[sheetId]) {
                const newRangesXc = this.getters.getAdaptedCfRanges(sheetId, cf, toAdd, toRemove);
                if (!newRangesXc) {
                    continue;
                }
                if (newRangesXc.length === 0) {
                    this.dispatch("REMOVE_CONDITIONAL_FORMAT", { id: cf.id, sheetId });
                    continue;
                }
                this.dispatch("ADD_CONDITIONAL_FORMAT", {
                    cf: {
                        id: cf.id,
                        rule: cf.rule,
                        stopIfTrue: cf.stopIfTrue,
                    },
                    ranges: newRangesXc,
                    sheetId,
                });
            }
        }
    }
    getCFToCopyTo(targetSheetId, originCF) {
        let targetCF = this.getters
            .getConditionalFormats(targetSheetId)
            .find((cf) => cf.stopIfTrue === originCF.stopIfTrue && deepEquals(cf.rule, originCF.rule));
        const queuedCfs = this.queuedChanges[targetSheetId];
        if (!targetCF && queuedCfs) {
            targetCF = queuedCfs.find((queued) => queued.cf.stopIfTrue === originCF.stopIfTrue && deepEquals(queued.cf.rule, originCF.rule))?.cf;
        }
        return targetCF || { ...originCF, id: this.uuidGenerator.smallUuid(), ranges: [] };
    }
}

class DataValidationClipboardHandler extends AbstractCellClipboardHandler {
    uuidGenerator = new UuidGenerator();
    queuedChanges = {};
    copy(data) {
        const { rowsIndexes, columnsIndexes } = data;
        const sheetId = data.sheetId;
        const dvRules = [];
        for (const row of rowsIndexes) {
            const dvRuleInRow = [];
            for (const col of columnsIndexes) {
                const position = { sheetId, col, row };
                const rule = this.getters.getValidationRuleForCell(position);
                dvRuleInRow.push({ position, rule });
            }
            dvRules.push(dvRuleInRow);
        }
        return { dvRules };
    }
    paste(target, clippedContent, options) {
        this.queuedChanges = {};
        if (options.pasteOption) {
            return;
        }
        const zones = target.zones;
        const sheetId = target.sheetId;
        if (!options.isCutOperation) {
            this.pasteFromCopy(sheetId, zones, clippedContent.dvRules);
        }
        else {
            this.pasteFromCut(sheetId, zones, clippedContent);
        }
        this.executeQueuedChanges();
    }
    pasteFromCut(sheetId, target, content) {
        const selection = target[0];
        this.pasteZone(sheetId, selection.left, selection.top, content.dvRules, {
            isCutOperation: true,
        });
    }
    pasteZone(sheetId, col, row, dvRules, clipboardOptions) {
        for (const [r, rowCells] of dvRules.entries()) {
            for (const [c, origin] of rowCells.entries()) {
                const position = { col: col + c, row: row + r, sheetId };
                this.pasteDataValidation(origin, position, clipboardOptions?.isCutOperation);
            }
        }
    }
    pasteDataValidation(origin, target, isCutOperation) {
        if (origin) {
            const zone = positionToZone(target);
            const originZone = positionToZone(origin.position);
            const rule = origin.rule;
            if (!rule) {
                const targetRule = this.getters.getValidationRuleForCell(target);
                if (targetRule) {
                    // Remove the data validation rule on the target cell
                    this.adaptDataValidationRule(target.sheetId, targetRule, [], [zone]);
                }
                return;
            }
            const toRemoveZone = [];
            if (isCutOperation) {
                toRemoveZone.push(originZone);
            }
            if (origin.position.sheetId === target.sheetId) {
                const copyToRule = this.getDataValidationRuleToCopyTo(target.sheetId, rule, false);
                this.adaptDataValidationRule(origin.position.sheetId, copyToRule, [zone], toRemoveZone);
            }
            else {
                const originRule = this.getters.getValidationRuleForCell(origin.position);
                if (originRule) {
                    this.adaptDataValidationRule(origin.position.sheetId, originRule, [], toRemoveZone);
                }
                const copyToRule = this.getDataValidationRuleToCopyTo(target.sheetId, rule);
                this.adaptDataValidationRule(target.sheetId, copyToRule, [zone], []);
            }
        }
    }
    getDataValidationRuleToCopyTo(targetSheetId, originRule, newId = true) {
        let targetRule = this.getters
            .getDataValidationRules(targetSheetId)
            .find((rule) => deepEquals(originRule.criterion, rule.criterion) &&
            originRule.isBlocking === rule.isBlocking);
        const queuedRules = this.queuedChanges[targetSheetId];
        if (!targetRule && queuedRules) {
            targetRule = queuedRules.find((queued) => deepEquals(originRule.criterion, queued.rule.criterion) &&
                originRule.isBlocking === queued.rule.isBlocking)?.rule;
        }
        return (targetRule || {
            ...originRule,
            id: newId ? this.uuidGenerator.smallUuid() : originRule.id,
            ranges: [],
        });
    }
    /**
     * Add or remove XCs to a given data validation rule.
     */
    adaptDataValidationRule(sheetId, rule, toAdd, toRemove) {
        if (!this.queuedChanges[sheetId]) {
            this.queuedChanges[sheetId] = [];
        }
        const queuedChange = this.queuedChanges[sheetId].find((queued) => queued.rule.id === rule.id);
        if (!queuedChange) {
            this.queuedChanges[sheetId].push({ toAdd, toRemove, rule });
        }
        else {
            queuedChange.toAdd.push(...toAdd);
            queuedChange.toRemove.push(...toRemove);
        }
    }
    executeQueuedChanges() {
        for (const sheetId in this.queuedChanges) {
            for (const { toAdd, toRemove, rule: dv } of this.queuedChanges[sheetId]) {
                // Remove the zones first in case the same position is in toAdd and toRemove
                const dvZones = dv.ranges.map((range) => range.zone);
                const withRemovedZones = recomputeZones(dvZones, toRemove);
                const newDvZones = recomputeZones([...withRemovedZones, ...toAdd], []);
                if (newDvZones.length === 0) {
                    this.dispatch("REMOVE_DATA_VALIDATION_RULE", { sheetId, id: dv.id });
                    continue;
                }
                this.dispatch("ADD_DATA_VALIDATION_RULE", {
                    rule: { id: dv.id, criterion: dv.criterion, isBlocking: dv.isBlocking },
                    ranges: newDvZones.map((zone) => this.getters.getRangeDataFromZone(sheetId, zone)),
                    sheetId,
                });
            }
        }
    }
}

class ImageClipboardHandler extends AbstractFigureClipboardHandler {
    copy(data) {
        const sheetId = data.sheetId;
        const figure = this.getters.getFigure(sheetId, data.figureId);
        if (!figure) {
            throw new Error(`No figure for the given id: ${data.figureId}`);
        }
        const copiedFigure = { ...figure };
        if (figure.tag !== "image") {
            return;
        }
        const image = this.getters.getImage(data.figureId);
        const copiedImage = deepCopy(image);
        return {
            figureId: data.figureId,
            copiedFigure,
            copiedImage,
            sheetId,
        };
    }
    getPasteTarget(sheetId, target, content, options) {
        const newId = new UuidGenerator().smallUuid();
        return { sheetId, zones: [], figureId: newId };
    }
    paste(target, clippedContent, options) {
        if (!target.figureId) {
            return;
        }
        const { zones, figureId } = target;
        const sheetId = this.getters.getActiveSheetId();
        const { width, height } = clippedContent.copiedFigure;
        const copy = deepCopy(clippedContent.copiedImage);
        const maxPosition = this.getters.getMaxAnchorOffset(sheetId, height, width);
        let { left: col, top: row } = zones[0];
        const offset = { x: 0, y: 0 };
        if (col > maxPosition.col) {
            col = maxPosition.col;
            offset.x = maxPosition.offset.x;
        }
        if (row > maxPosition.row) {
            row = maxPosition.row;
            offset.y = maxPosition.offset.y;
        }
        this.dispatch("CREATE_IMAGE", {
            figureId,
            sheetId,
            col,
            row,
            offset,
            size: { height, width },
            definition: copy,
        });
        if (options.isCutOperation) {
            this.dispatch("DELETE_FIGURE", {
                sheetId: clippedContent.sheetId,
                figureId: clippedContent.copiedFigure.id,
            });
        }
        this.dispatch("SELECT_FIGURE", { figureId });
    }
    isPasteAllowed(sheetId, target, content, option) {
        if (target.length === 0) {
            return "EmptyTarget" /* CommandResult.EmptyTarget */;
        }
        if (option?.pasteOption !== undefined) {
            return "WrongFigurePasteOption" /* CommandResult.WrongFigurePasteOption */;
        }
        return "Success" /* CommandResult.Success */;
    }
}

class MergeClipboardHandler extends AbstractCellClipboardHandler {
    copy(data) {
        const sheetId = this.getters.getActiveSheetId();
        const { rowsIndexes, columnsIndexes } = data;
        const merges = [];
        for (const row of rowsIndexes) {
            const mergesInRow = [];
            for (const col of columnsIndexes) {
                const position = { col, row, sheetId };
                mergesInRow.push(this.getters.getMerge(position));
            }
            merges.push(mergesInRow);
        }
        return { merges, sheetId };
    }
    /**
     * Paste the clipboard content in the given target
     */
    paste(target, content, options) {
        if (options.isCutOperation) {
            const copiedMerges = content.merges.flat().filter(isDefined);
            this.dispatch("REMOVE_MERGE", { sheetId: content.sheetId, target: copiedMerges });
        }
        this.pasteFromCopy(target.sheetId, target.zones, content.merges, options);
    }
    pasteZone(sheetId, col, row, merges) {
        for (const [r, rowMerges] of merges.entries()) {
            for (const [c, originMerge] of rowMerges.entries()) {
                const position = { col: col + c, row: row + r, sheetId };
                this.pasteMerge(originMerge, position);
            }
        }
    }
    pasteMerge(originMerge, target) {
        if (!originMerge) {
            return;
        }
        if (this.getters.isInMerge(target)) {
            return;
        }
        const { sheetId, col, row } = target;
        this.dispatch("ADD_MERGE", {
            sheetId,
            force: true,
            target: [
                {
                    left: col,
                    top: row,
                    right: col + originMerge.right - originMerge.left,
                    bottom: row + originMerge.bottom - originMerge.top,
                },
            ],
        });
    }
}

class ReferenceClipboardHandler extends AbstractCellClipboardHandler {
    copy(data) {
        return {
            zones: data.clippedZones,
            sheetId: data.sheetId,
        };
    }
    paste(target, content, options) {
        if (options.isCutOperation) {
            const selection = target.zones[0];
            this.dispatch("MOVE_RANGES", {
                target: content.zones,
                sheetId: content.sheetId,
                sheetName: this.getters.getSheetName(content.sheetId),
                targetSheetId: target.sheetId,
                col: selection.left,
                row: selection.top,
            });
        }
    }
}

class SheetClipboardHandler extends AbstractCellClipboardHandler {
    isPasteAllowed(sheetId, target, content, options) {
        if (!("cells" in content)) {
            return "Success" /* CommandResult.Success */;
        }
        const { xSplit, ySplit } = this.getters.getPaneDivisions(sheetId);
        const pasteZones = getPasteZones(target, content.cells);
        if (doesAnyZoneCrossFrozenPane(pasteZones, xSplit, ySplit)) {
            return "FrozenPaneOverlap" /* CommandResult.FrozenPaneOverlap */;
        }
        return "Success" /* CommandResult.Success */;
    }
}

class TableClipboardHandler extends AbstractCellClipboardHandler {
    copy(data, isCutOperation, mode = "copyPaste") {
        const sheetId = data.sheetId;
        const { rowsIndexes, columnsIndexes, zones } = data;
        const copiedTablesIds = new Set();
        const tableCells = [];
        for (const row of rowsIndexes) {
            const tableCellsInRow = [];
            tableCells.push(tableCellsInRow);
            for (const col of columnsIndexes) {
                const position = { col, row, sheetId };
                const table = this.getters.getTable(position);
                if (!table) {
                    tableCellsInRow.push({});
                    continue;
                }
                const coreTable = this.getters.getCoreTable(position);
                const tableZone = coreTable?.range.zone;
                let copiedTable = undefined;
                // Copy whole table
                if (!copiedTablesIds.has(table.id) &&
                    coreTable &&
                    tableZone &&
                    zones.some((z) => isZoneInside(tableZone, z))) {
                    copiedTablesIds.add(table.id);
                    let { numberOfRows } = zoneToDimension(tableZone);
                    for (let rowIndex = tableZone.top; rowIndex <= tableZone.bottom; rowIndex++) {
                        if (!isCutOperation && !rowsIndexes.includes(rowIndex)) {
                            numberOfRows--;
                        }
                    }
                    const range = coreTable.range;
                    const newRange = this.getters.extendRange(coreTable.range, "ROW", range.zone.top + numberOfRows - 1 - range.zone.bottom);
                    copiedTable = {
                        range: this.getters.getRangeData(newRange),
                        config: coreTable.config,
                        type: coreTable.type,
                    };
                }
                if (mode !== "shiftCells") {
                    tableCellsInRow.push({
                        table: copiedTable,
                        style: this.getTableStyleToCopy(position),
                        isWholeTableCopied: copiedTablesIds.has(table.id),
                    });
                }
            }
        }
        return {
            tableCells,
            sheetId: data.sheetId,
        };
    }
    /**
     * Get the style to copy for a cell. We need to copy both the table style and the cell style, because
     * UPDATE_CELL replace the whole style of the cell with the style of the command, it doesn't merge the two.
     */
    getTableStyleToCopy(cellPosition) {
        const styleFromTable = this.getters.getCellTableStyle(cellPosition);
        const cellStyle = this.getters.getCellStyle(cellPosition);
        const bordersFromTable = this.getters.getCellTableBorder(cellPosition);
        const cellBorder = this.getters.getCellBorder(cellPosition);
        return {
            style: { ...styleFromTable, ...removeFalsyAttributes(cellStyle) },
            border: { ...bordersFromTable, ...removeFalsyAttributes(cellBorder) },
        };
    }
    paste(target, content, options) {
        const zones = target.zones;
        const sheetId = target.sheetId;
        if (!options.isCutOperation) {
            this.pasteFromCopy(sheetId, zones, content.tableCells, options);
        }
        else {
            this.pasteFromCut(sheetId, zones, content, options);
        }
    }
    pasteFromCut(sheetId, target, content, options) {
        for (const row of content.tableCells) {
            for (const tableCell of row) {
                if (tableCell.table) {
                    this.dispatch("REMOVE_TABLE", {
                        sheetId: content.sheetId,
                        target: [this.getters.getRangeFromRangeData(tableCell.table.range).zone],
                    });
                }
            }
        }
        const selection = target[0];
        this.pasteZone(sheetId, selection.left, selection.top, content.tableCells, options);
    }
    pasteZone(sheetId, col, row, tableCells, clipboardOptions) {
        for (let r = 0; r < tableCells.length; r++) {
            const rowCells = tableCells[r];
            for (let c = 0; c < rowCells.length; c++) {
                const tableCell = rowCells[c];
                if (!tableCell) {
                    continue;
                }
                const position = { col: col + c, row: row + r, sheetId };
                this.pasteTableCell(sheetId, tableCell, position, clipboardOptions);
            }
        }
        if (tableCells.length === 1) {
            for (let c = 0; c < tableCells[0].length; c++) {
                this.dispatch("AUTOFILL_TABLE_COLUMN", { col: col + c, row, sheetId });
            }
        }
    }
    pasteTableCell(sheetId, tableCell, position, options) {
        if (tableCell.table && !options?.pasteOption) {
            const { range: tableRange } = tableCell.table;
            const zoneDims = zoneToDimension(this.getters.getRangeFromRangeData(tableRange).zone);
            const newTableZone = {
                left: position.col,
                top: position.row,
                right: position.col + zoneDims.numberOfCols - 1,
                bottom: position.row + zoneDims.numberOfRows - 1,
            };
            this.dispatch("CREATE_TABLE", {
                sheetId: position.sheetId,
                ranges: [this.getters.getRangeDataFromZone(sheetId, newTableZone)],
                config: tableCell.table.config,
                tableType: tableCell.table.type,
            });
        }
        // We cannot check for dynamic tables, because at this point the paste can have changed the evaluation, and the
        // dynamic tables are not yet computed
        if (this.getters.getCoreTable(position) || options?.pasteOption === "asValue") {
            return;
        }
        if ((!options?.pasteOption && !tableCell.isWholeTableCopied) ||
            options?.pasteOption === "onlyFormat") {
            if (tableCell.style?.style) {
                this.dispatch("UPDATE_CELL", { ...position, style: tableCell.style.style });
            }
            if (tableCell.style?.border) {
                this.dispatch("SET_BORDER", { ...position, border: tableCell.style.border });
            }
        }
    }
}

const clipboardHandlersRegistries = {
    figureHandlers: new Registry(),
    cellHandlers: new Registry(),
};
clipboardHandlersRegistries.figureHandlers
    .add("chart", ChartClipboardHandler)
    .add("image", ImageClipboardHandler)
    .add("carousel", CarouselClipboardHandler);
clipboardHandlersRegistries.cellHandlers
    .add("dataValidation", DataValidationClipboardHandler)
    .add("cell", CellClipboardHandler)
    .add("sheet", SheetClipboardHandler)
    .add("merge", MergeClipboardHandler)
    .add("border", BorderClipboardHandler)
    .add("table", TableClipboardHandler)
    .add("conditionalFormat", ConditionalFormatClipboardHandler)
    .add("references", ReferenceClipboardHandler);

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

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

/**
 * A type-safe dependency container
 */
class DependencyContainer extends EventBus {
    dependencies = new Map();
    factory = new StoreFactory(this.get.bind(this));
    /**
     * Injects a store instance in the dependency container.
     * Useful for injecting an external store that is not created by the container.
     * Also useful for mocking a store.
     */
    inject(Store, instance) {
        if (this.dependencies.has(Store) && this.dependencies.get(Store) !== instance) {
            throw new Error(`Store ${Store.name} already has an instance`);
        }
        this.dependencies.set(Store, instance);
    }
    /**
     * Get an instance of a store.
     */
    get(Store) {
        if (!this.dependencies.has(Store)) {
            this.dependencies.set(Store, this.instantiate(Store));
        }
        return this.dependencies.get(Store);
    }
    instantiate(Store, ...args) {
        return this.factory.build(Store, ...args);
    }
    resetStores() {
        this.dependencies.clear();
    }
    dispose() {
        for (const instance of this.dependencies.values()) {
            if ("dispose" in instance && typeof instance.dispose === "function") {
                instance.dispose();
            }
        }
    }
}
class StoreFactory {
    get;
    pendingBuilds = new Set();
    constructor(get) {
        this.get = get;
    }
    /**
     * Build a store instance and get all its dependencies
     * while detecting and preventing circular dependencies
     */
    build(Store, ...args) {
        if (this.pendingBuilds.has(Store)) {
            throw new Error(`Circular dependency detected: ${[...this.pendingBuilds, Store]
                .map((s) => s.name)
                .join(" -> ")}`);
        }
        this.pendingBuilds.add(Store);
        const instance = new Store(this.get, ...args);
        this.pendingBuilds.delete(Store);
        return instance;
    }
}

/**
 * Create a store to expose an external resource (which is not a store itself)
 * to other stores.
 * The external resource needs to be injected in the store provider to provide
 * the store implementation.
 *
 * @example
 * const MyMetaStore = createAbstractStore("MyStore");
 * const stores = useStoreProvider();
 * stores.inject(MyMetaStore, externalResourceInstance);
 */
function createAbstractStore(storeName) {
    class MetaStore {
        constructor(get) {
            throw new Error(`This is a abstract store for ${storeName}, it cannot be instantiated.
Did you forget to inject your store instance?

const stores = useStoreProvider();
stores.inject(MyMetaStore, storeInstance);
`);
        }
    }
    return MetaStore;
}
class DisposableStore {
    get;
    disposeCallbacks = [];
    constructor(get) {
        this.get = get;
    }
    onDispose(callback) {
        this.disposeCallbacks.push(callback);
    }
    dispose() {
        this.disposeCallbacks.forEach((cb) => cb());
    }
}

/**
 * This hook should be used at the root of your app to provide the store container.
 */
function useStoreProvider() {
    const env = useEnv();
    if (env.__spreadsheet_stores__ instanceof DependencyContainer) {
        return env.__spreadsheet_stores__;
    }
    const container = new DependencyContainer();
    useSubEnv({
        __spreadsheet_stores__: container,
        getStore: (Store) => {
            const store = container.get(Store);
            return proxifyStoreMutation(store, () => container.trigger("store-updated"));
        },
    });
    onWillUnmount(() => container.dispose());
    return container;
}
/**
 * Get the instance of a store.
 */
function useStore(Store) {
    const env = useEnv();
    const container = getDependencyContainer(env);
    const store = container.get(Store);
    return useStoreRenderProxy(container, store);
}
function useLocalStore(Store, ...args) {
    const env = useEnv();
    const container = getDependencyContainer(env);
    const store = container.instantiate(Store, ...args);
    onWillUnmount(() => store.dispose());
    return useStoreRenderProxy(container, store);
}
/**
 * Trigger an event to re-render the app (deep render) when
 * a store is mutated by invoking one of its mutator methods.
 */
function useStoreRenderProxy(container, store) {
    const component = useComponent();
    const proxy = proxifyStoreMutation(store, () => {
        if (status(component) === "mounted") {
            container.trigger("store-updated");
        }
    });
    return proxy;
}
/**
 * Creates a proxied version of a store object with mutation tracking.
 * Whenever a mutator method of the store is called, the provided callback function is invoked.
 */
function proxifyStoreMutation(store, callback) {
    const proxy = new Proxy(store, {
        get(target, property, receiver) {
            const thisStore = target;
            // The third argument is `thisStore` (target) instead of `receiver`.
            // The goal is to always have the same `this` value in getter functions
            // (when `target[property]` is an accessor property).
            // `thisStore` is always the same object reference. `receiver` however is the
            // object on which the property is called, which is the Proxy object which is different for each component.
            const value = Reflect.get(target, property, thisStore);
            if (store.mutators?.includes(property)) {
                const functionProxy = new Proxy(value, {
                    // trap the function call
                    apply(target, thisArg, argArray) {
                        const res = Reflect.apply(target, thisStore, argArray);
                        if (res === "noStateChange") {
                            return;
                        }
                        callback();
                    },
                });
                return functionProxy;
            }
            return value;
        },
    });
    return proxy;
}
function getDependencyContainer(env) {
    const container = env.__spreadsheet_stores__;
    if (!(container instanceof DependencyContainer)) {
        throw new Error("No store provider found. Did you forget to call useStoreProvider()?");
    }
    return container;
}

const ModelStore = createAbstractStore("Model");

class RendererStore {
    mutators = ["register", "unRegister", "draw", "startAnimation", "stopAnimation"];
    renderers = {};
    model;
    context = undefined;
    animationFrameId = null;
    registeredAnimations = new Set();
    constructor(get) {
        this.model = get(ModelStore);
    }
    register(renderer) {
        if (!renderer.renderingLayers.length) {
            return;
        }
        for (const layer of renderer.renderingLayers) {
            if (!this.renderers[layer]) {
                this.renderers[layer] = [];
            }
            this.renderers[layer].push(renderer);
        }
    }
    unRegister(renderer) {
        for (const layer of Object.keys(this.renderers)) {
            this.renderers[layer] = this.renderers[layer].filter((r) => r !== renderer);
        }
    }
    drawLayer(context, layer, timeStamp) {
        const renderers = this.renderers[layer];
        if (renderers) {
            for (const renderer of renderers) {
                context.ctx.save();
                renderer.drawLayer(context, layer, timeStamp);
                context.ctx.restore();
            }
        }
        return "noStateChange";
    }
    draw(context, timestamp) {
        context = context || this.context;
        if (!context) {
            throw new Error("Rendering context is not defined");
        }
        this.context = context;
        for (const layer of OrderedLayers()) {
            this.model.drawLayer(context, layer);
            this.drawLayer(context, layer, timestamp);
        }
        return "noStateChange";
    }
    startAnimation(animationId) {
        this.registeredAnimations.add(animationId);
        if (!this.animationFrameId) {
            const animationCallback = (timestamp) => {
                this.animationFrameId = requestAnimationFrame(animationCallback);
                this.draw(undefined, timestamp);
            };
            this.animationFrameId = requestAnimationFrame(animationCallback);
        }
        return "noStateChange";
    }
    stopAnimation(animationId) {
        this.registeredAnimations.delete(animationId);
        if (this.registeredAnimations.size === 0 && this.animationFrameId !== null) {
            cancelAnimationFrame(this.animationFrameId);
            this.animationFrameId = null;
        }
        return "noStateChange";
    }
    dispose() {
        if (this.animationFrameId) {
            cancelAnimationFrame(this.animationFrameId);
            this.animationFrameId = null;
        }
    }
}

class SpreadsheetStore extends DisposableStore {
    // cast the model store as Model to allow model.dispatch to return the DispatchResult
    model = this.get(ModelStore);
    getters = this.model.getters;
    renderer = this.get(RendererStore);
    constructor(get) {
        super(get);
        this.model.on("command-dispatched", this, this.handle);
        this.model.on("command-finalized", this, this.finalize);
        this.renderer.register(this);
        this.onDispose(() => {
            this.model.off("command-dispatched", this);
            this.model.off("command-finalized", this);
            this.renderer.unRegister(this);
        });
    }
    get renderingLayers() {
        return [];
    }
    handle(cmd) { }
    finalize() { }
    drawLayer(ctx, layer, timestamp) { }
}

const VOID_COMPOSER = {
    id: "void-composer",
    get editionMode() {
        return "inactive";
    },
    startEdition: () => {
        throw new Error("No composer is registered");
    },
    stopEdition: () => {
        throw new Error("No composer is registered");
    },
    setCurrentContent: () => {
        throw new Error("No composer is registered");
    },
};
class ComposerFocusStore extends SpreadsheetStore {
    mutators = ["focusComposer", "focusActiveComposer"];
    activeComposer = VOID_COMPOSER;
    _focusMode = "inactive";
    get focusMode() {
        return this.activeComposer.editionMode === "inactive" ? "inactive" : this._focusMode;
    }
    focusComposer(listener, args) {
        this.activeComposer = listener;
        if (this.getters.isReadonly()) {
            return "noStateChange";
        }
        this._focusMode = args.focusMode || "contentFocus";
        if (this._focusMode !== "inactive") {
            this.setComposerContent(args);
        }
        return;
    }
    focusActiveComposer(args) {
        if (this.getters.isReadonly()) {
            return "noStateChange";
        }
        if (!this.activeComposer) {
            throw new Error("No composer is registered");
        }
        this._focusMode = args.focusMode || "contentFocus";
        if (this._focusMode !== "inactive") {
            this.setComposerContent(args);
        }
        return;
    }
    /**
     * Start the edition or update the content if it's already started.
     */
    setComposerContent({ content, selection, }) {
        if (this.activeComposer.editionMode === "inactive") {
            this.activeComposer.startEdition(content, selection);
        }
        else if (content) {
            this.activeComposer.setCurrentContent(content, selection);
        }
    }
}

/**
 * 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) {
        const token = tokens.shift();
        if (token === "}") {
            generateRules();
            selectorStack.pop();
        }
        else {
            if (tokens[0] === "{") {
                generateRules();
                selectorStack.push(token.split(/\s*,\s*/));
                tokens.shift();
            }
            if (tokens[0] === ";") {
                rules.push("  " + token + ";");
            }
        }
    }
    return parts.join("\n");
}
function registerSheet(id, css) {
    const sheet = document.createElement("style");
    sheet.textContent = processSheet(css);
    STYLESHEETS[id] = sheet;
}
function activateSheet(id) {
    const sheet = STYLESHEETS[id];
    sheet.setAttribute("component", id);
    document.head.appendChild(sheet);
}
function getTextDecoration({ strikethrough, underline, }) {
    if (!strikethrough && !underline) {
        return "none";
    }
    return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`;
}
/**
 * Convert the cell style to CSS properties.
 */
function cellStyleToCss(style) {
    const attributes = cellTextStyleToCss(style);
    if (!style)
        return attributes;
    if (style.fillColor) {
        attributes["background"] = style.fillColor;
    }
    return attributes;
}
/**
 * Convert the cell text style to CSS properties.
 */
function cellTextStyleToCss(style) {
    const attributes = {};
    if (!style)
        return attributes;
    if (style.bold) {
        attributes["font-weight"] = "bold";
    }
    if (style.italic) {
        attributes["font-style"] = "italic";
    }
    if (style.fontSize) {
        attributes["font-size"] = `${style.fontSize}px`;
    }
    if (style.strikethrough || style.underline) {
        let decoration = style.strikethrough ? "line-through" : "";
        decoration = style.underline ? decoration + " underline" : decoration;
        attributes["text-decoration"] = decoration;
    }
    if (style.textColor) {
        attributes["color"] = style.textColor;
    }
    return attributes;
}
/**
 * Transform CSS properties into a CSS string.
 */
function cssPropertiesToCss(attributes) {
    let styleStr = "";
    for (const attName in attributes) {
        if (!attributes[attName]) {
            continue;
        }
        styleStr += `${attName}:${attributes[attName]}; `;
    }
    return styleStr;
}
function getElementMargins(el) {
    const style = window.getComputedStyle(el);
    return {
        top: parseInt(style.marginTop, 10) || 0,
        bottom: parseInt(style.marginBottom, 10) || 0,
        left: parseInt(style.marginLeft, 10) || 0,
        right: parseInt(style.marginRight, 10) || 0,
    };
}

const chartJsExtensionRegistry = new Registry();
function areChartJSExtensionsLoaded() {
    return !!window.Chart.registry.plugins.get("chartShowValuesPlugin");
}
function registerChartJSExtensions() {
    if (!window.Chart || areChartJSExtensionsLoaded()) {
        return;
    }
    for (const registryItem of chartJsExtensionRegistry.getAll()) {
        registryItem.register(window.Chart);
    }
}
function unregisterChartJsExtensions() {
    if (!window.Chart) {
        return;
    }
    for (const registryItem of chartJsExtensionRegistry.getAll()) {
        registryItem.unregister(window.Chart);
    }
}

class ChartAnimationStore extends SpreadsheetStore {
    mutators = ["disableAnimationForChart", "enableAnimationForChart"];
    animationPlayed = {};
    disableAnimationForChart(chartId, chartType) {
        this.animationPlayed[chartId] = chartType;
        return "noStateChange";
    }
    enableAnimationForChart(chartId) {
        this.animationPlayed[chartId] = undefined;
        return "noStateChange";
    }
}

function getFunnelChartController() {
    return class FunnelChartController extends window.Chart.BarController {
        static id = "funnel";
        static defaults = {
            ...window.Chart?.BarController.defaults,
            dataElementType: "funnel",
            animation: {
                duration: (ctx) => {
                    if (ctx.type !== "data") {
                        return 1000;
                    }
                    const value = ctx.raw[1];
                    const maxValue = Math.max(...ctx.dataset.data.map((data) => data[1]));
                    return 1000 * (value / maxValue);
                },
            },
        };
        /** Called at each chart render to update the elements of the chart (FunnelChartElement) with the updated data */
        updateElements(rects, start, count, mode) {
            super.updateElements(rects, start, count, mode);
            for (let i = start; i < start + count; i++) {
                const rect = rects[i];
                // Add the next element to the element's props so we can get the bottom width of the trapezoid
                this.updateElement(rect, i, { nextElement: rects[i + 1] }, mode);
            }
        }
    };
}
function getFunnelChartElement() {
    /**
     * Similar to a bar chart element, but it's a trapezoid rather than a rectangle. The top is of width
     * `width`, and the bottom is of width `nextElementWidth`.
     */
    return class FunnelChartElement extends window.Chart.BarElement {
        static id = "funnel";
        /** Overwrite this to draw a trapezoid rather then a rectangle */
        draw(ctx) {
            ctx.save();
            const props = ["x", "y", "width", "height", "nextElement", "base", "options"];
            const { x, y, height, nextElement, base, options } = this.getProps(props);
            const width = getElementWidth(this);
            const nextElementWidth = nextElement ? getElementWidth(nextElement) : 0;
            const offset = (width - nextElementWidth) / 2;
            const startX = Math.min(x, base);
            const startY = y - height / 2;
            ctx.fillStyle = options.backgroundColor;
            ctx.beginPath();
            ctx.moveTo(startX, startY);
            ctx.lineTo(startX + width, startY);
            ctx.lineTo(startX + width - offset, startY + height);
            ctx.lineTo(startX + offset, startY + height);
            ctx.closePath();
            ctx.fill();
            if (options.borderWidth) {
                ctx.strokeStyle = options.borderColor;
                ctx.lineWidth = options.borderWidth;
                ctx.stroke();
            }
            ctx.restore();
        }
        /** Check if the mouse is inside the trapezoid */
        inRange(mouseX, mouseY) {
            const props = ["x", "y", "width", "height", "nextElement", "base", "options"];
            const { x, y, height, nextElement, base } = this.getProps(props);
            const width = getElementWidth(this);
            const nextElementWidth = nextElement ? getElementWidth(nextElement) : 0;
            const startX = Math.min(x, base);
            const startY = y - height / 2;
            if (mouseY < startY || mouseY > startY + height) {
                return false;
            }
            const offset = (width - nextElementWidth) / 2;
            const left = startX + (offset * (mouseY - startY)) / height;
            const right = startX + width - (offset * (mouseY - startY)) / height;
            if (mouseX < left || mouseX > right) {
                return false;
            }
            return true;
        }
    };
}
/**
 * Get an element width.
 *
 * The property width is undefined during animations, we need to compute it manually.
 */
function getElementWidth(element) {
    const { x, base } = element.getProps(["x", "base"]);
    const left = Math.min(x, base);
    const right = Math.max(x, base);
    return right - left;
}
/**
 * Position the tooltip inside the trapezoid.
 * The default position for tooltips of bar elements is at the end of rectangle, which is not ideal for trapezoids.
 */
const funnelTooltipPositioner = function (elements) {
    if (!elements.length) {
        return { x: 0, y: 0 };
    }
    const element = elements[0].element;
    const { x, y, base, width, height } = element.getProps(["x", "y", "width", "height", "base"]);
    const startX = Math.min(x, base);
    const startY = y - height / 2;
    return {
        x: startX + (width * 2) / 3,
        y: startY + height / 2,
    };
};

/** 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",
    macroEnabledWorkbook: "application/vnd.ms-excel.sheet.macroEnabled.main+xml",
    templateWorkbook: "application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml",
    macroEnabledTemplateWorkbook: "application/vnd.ms-excel.template.macroEnabled.main+xml",
    excelAddInWorkbook: "application/vnd.ms-excel.addin.macroEnabled.main+xml",
    sheet: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml",
    metadata: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+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",
    metadata: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMetadata",
    sharedStrings: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings",
    styles: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
    drawing: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
    chart: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart",
    theme: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme",
    table: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table",
    hyperlink: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
    image: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
};
const ARRAY_FORMULA_URI = "bdbb8cdc-fa1e-496e-a857-3c3f30c029c3";
const RELATIONSHIP_NSR = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
const HEIGHT_FACTOR = 0.75; // 100px => 75 u
/**
 * Excel says its default column width is 8.43 characters (64px)
 * which makes WIDTH_FACTOR = 0.1317, but it doesn't work well
 * 0.143 is a value from dev's experiments.
 */
const WIDTH_FACTOR = 0.143;
/** unit : maximum number of characters a column can hold at the standard font size. What. */
const EXCEL_DEFAULT_COL_WIDTH = 8.43;
/** unit : points */
const EXCEL_DEFAULT_ROW_HEIGHT = 12.75;
const EXCEL_IMPORT_DEFAULT_NUMBER_OF_COLS = 30;
const EXCEL_IMPORT_DEFAULT_NUMBER_OF_ROWS = 100;
/** The possible values for the XLSX polynomial trendline order are defined by the ST_Order simple type (§21.2.3.29) */
const MAX_XLSX_POLYNOMIAL_DEGREE = 6;
const FIRST_NUMFMT_ID = 164;
const DEFAULT_DOUGHNUT_CHART_HOLE_SIZE = 50;
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";

//------------------------------------------------------------------------------
// Arg description DSL
//------------------------------------------------------------------------------
const ARG_REGEXP = /(.*?)\((.*?)\)(.*)/;
const ARG_TYPES = [
    "ANY",
    "BOOLEAN",
    "DATE",
    "NUMBER",
    "STRING",
    "RANGE",
    "RANGE<BOOLEAN>",
    "RANGE<DATE>",
    "RANGE<NUMBER>",
    "RANGE<STRING>",
    "META",
    "RANGE<META>",
];
function arg(definition, description = "", proposals) {
    return makeArg(definition, description, proposals);
}
function makeArg(str, description, proposals) {
    const parts = str.match(ARG_REGEXP);
    const name = parts[1].trim();
    if (!name) {
        throw new Error(`Function argument definition is missing a name: '${str}'.`);
    }
    const types = [];
    let isOptional = false;
    let isRepeating = false;
    let defaultValue;
    for (const param of parts[2].split(",")) {
        const key = param.trim().toUpperCase();
        const 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.startsWith("DEFAULT=")) {
            defaultValue = param.trim().slice(8);
        }
    }
    const result = {
        name,
        description,
        type: types,
    };
    const acceptErrors = types.includes("ANY") || types.includes("RANGE");
    if (acceptErrors) {
        result.acceptErrors = true;
    }
    if (isOptional) {
        result.optional = true;
    }
    if (isRepeating) {
        result.repeating = true;
    }
    if (defaultValue !== undefined) {
        result.default = true;
        result.defaultValue = defaultValue;
    }
    if (types.some((t) => t.startsWith("RANGE"))) {
        result.acceptMatrix = true;
    }
    if (types.every((t) => t.startsWith("RANGE"))) {
        result.acceptMatrixOnly = true;
    }
    if (proposals && proposals.length > 0) {
        result.proposalValues = proposals;
    }
    return result;
}
/**
 * This function adds on description more general information derived from the
 * arguments.
 *
 * This information is useful during compilation.
 */
function addMetaInfoFromArg(name, addDescr) {
    let countArg = 0;
    let minArg = 0;
    let repeatingArg = 0;
    let optionalArg = 0;
    for (const arg of addDescr.args) {
        countArg++;
        if (!arg.optional && !arg.repeating && !arg.default) {
            minArg++;
        }
        if (arg.repeating) {
            repeatingArg++;
        }
        if (arg.optional || arg.default) {
            optionalArg++;
        }
    }
    const descr = addDescr;
    descr.minArgRequired = minArg;
    descr.maxArgPossible = repeatingArg ? Infinity : countArg;
    descr.nbrArgRepeating = repeatingArg;
    descr.nbrArgOptional = optionalArg;
    descr.hidden = addDescr.hidden || false;
    descr.name = name;
    return descr;
}
const cacheArgTargeting = {};
/**
 * Returns a function that maps the position of a value in a function to its corresponding argument index.
 *
 * In most cases, the task is straightforward:
 *
 * In the formula "=SUM(11, 55, 66)" which is defined like this "SUM(value1, [value2, ...])":
 * - 11 corresponds to the value1 argument => position will be 0
 * - 55 and 66 correspond to the [value2, ...] argument => position will be 1
 *
 * In other cases, optional arguments could be defined after repeatable arguments,
 * or even optional and required arguments could be mixed in unconventional ways.
 *
 * The next function has been designed to handle all possible configurations.
 * The only restriction is if repeatable arguments are present in the function definition:
 * - they must be defined consecutively
 * - they must be in a quantity greater than the optional arguments.
 *
 * The markdown tables below illustrate how values are mapped to positions based on the number of values supplied.
 * Each table represents a different function configuration, with columns representing the number of values supplied as arguments
 * and rows representing the correspondence with the argument index.
 *
 * The tables are built based on the following conventions:
 * - `m`: Mandatory argument
 * - `o`: Optional argument
 * - `r`: Repeating argument
 *
 *
 * Configuration 1: (m, o) like the CEILING function
 *
 * |   | 1 | 2 |
 * |---|---|---|
 * | m | 0 | 0 |
 * | o |   | 1 |
 *
 *
 * Configuration 2: (m, m, m, r, r) like the SUMIFS function
 *
 * |   | 3 | 5 | 7    | 3 + 2n     |
 * |---|---|---|------|------------|
 * | m | 0 | 0 | 0    | 0          |
 * | m | 1 | 1 | 1    | 1          |
 * | m | 2 | 2 | 2    | 2          |
 * | r |   | 3 | 3, 5 | 3 + 2n     |
 * | r |   | 4 | 4, 6 | 3 + 2n + 1 |
 *
 *
 * Configuration 3: (m, m, m, r, r, o) like the SWITCH function
 *
 * |   | 3 | 4 | 5 | 6 | 7    | 8    | 3 + 2n     | 3 + 2n + 1     |
 * |---|---|---|---|---|------|------|------------|----------------|
 * | m | 0 | 0 | 0 | 0 | 0    | 0    | 0          | 0              |
 * | m | 1 | 1 | 1 | 1 | 1    | 1    | 1          | 1              |
 * | m | 2 | 2 | 2 | 2 | 2    | 2    | 2          | 2              |
 * | r |   |   | 3 | 3 | 3, 5 | 3, 5 | 3 + 2n     | 3 + 2n         |
 * | r |   |   | 4 | 4 | 4, 6 | 4, 6 | 3 + 2n + 1 | 3 + 2n + 1     |
 * | o |   | 3 |   | 5 |      | 7    |            | 3 + 2N + 2     |
 *
 *
 * Configuration 4: (m, o, m, o, r, r, r, m) a complex case to understand subtleties
 *
 * |   | 3 | 4 | 5 | 6 | 7 | 8 | 9    | 10   | 11   | ... |
 * |---|---|---|---|---|---|---|------|------|------|-----|
 * | m | 0 | 0 | 0 | 0 | 0 | 0 | 0    | 0    | 0    | ... |
 * | o |   | 1 | 1 |   | 1 | 1 |      | 1    | 1    | ... |
 * | m | 1 | 2 | 2 | 1 | 2 | 2 | 1    | 2    | 2    | ... |
 * | o |   |   | 3 |   |   | 3 |      |      | 3    | ... |
 * | r |   |   |   | 2 | 3 | 4 | 2, 5 | 3, 6 | 4, 7 | ... |
 * | r |   |   |   | 3 | 4 | 5 | 3, 6 | 4, 7 | 5, 8 | ... |
 * | r |   |   |   | 4 | 5 | 6 | 4, 7 | 5, 8 | 6, 9 | ... |
 * | m | 2 | 3 | 4 | 5 | 6 | 7 | 8    | 9    | 10   | ... |
 *
 */
function argTargeting(functionDescription, nbrArgSupplied) {
    const functionName = functionDescription.name;
    const result = cacheArgTargeting[functionName]?.[nbrArgSupplied];
    if (result) {
        return result;
    }
    if (!cacheArgTargeting[functionName]) {
        cacheArgTargeting[functionName] = {};
    }
    if (!cacheArgTargeting[functionName][nbrArgSupplied]) {
        cacheArgTargeting[functionName][nbrArgSupplied] = _argTargeting(functionDescription, nbrArgSupplied);
    }
    return cacheArgTargeting[functionName][nbrArgSupplied];
}
function _argTargeting(functionDescription, nbrArgSupplied) {
    const valueIndexToArgPosition = {};
    const groupsOfRepeatingValues = functionDescription.nbrArgRepeating
        ? Math.floor((nbrArgSupplied - functionDescription.minArgRequired) / functionDescription.nbrArgRepeating)
        : 0;
    const nbrValueRepeating = functionDescription.nbrArgRepeating * groupsOfRepeatingValues;
    const nbrValueOptional = nbrArgSupplied - functionDescription.minArgRequired - nbrValueRepeating;
    let countValueSupplied = 0;
    let countValueOptional = 0;
    for (let i = 0; i < functionDescription.args.length; i++) {
        const arg = functionDescription.args[i];
        if (arg.optional || arg.default) {
            if (countValueOptional < nbrValueOptional) {
                valueIndexToArgPosition[countValueSupplied] = i;
                countValueSupplied++;
            }
            countValueOptional++;
            continue;
        }
        if (arg.repeating) {
            // As we know all repeating arguments are consecutive,
            // --> we will treat all repeating arguments in one go
            // --> the index i will be incremented by the number of repeating values at the end of the loop
            for (let j = 0; j < groupsOfRepeatingValues; j++) {
                for (let k = 0; k < functionDescription.nbrArgRepeating; k++) {
                    valueIndexToArgPosition[countValueSupplied] = i + k;
                    countValueSupplied++;
                }
            }
            i += functionDescription.nbrArgRepeating - 1;
            continue;
        }
        // End case: it's a required argument
        valueIndexToArgPosition[countValueSupplied] = i;
        countValueSupplied++;
    }
    return (argPosition) => {
        return valueIndexToArgPosition[argPosition];
    };
}
//------------------------------------------------------------------------------
// Argument validation
//------------------------------------------------------------------------------
const META_TYPES = ["META", "RANGE<META>"];
function validateArguments(descr) {
    if (descr.nbrArgRepeating && descr.nbrArgOptional >= descr.nbrArgRepeating) {
        throw new Error(`Function ${descr.name} has more optional arguments than repeatable ones.`);
    }
    let foundRepeating = false;
    let consecutiveRepeating = false;
    for (const current of descr.args) {
        if (current.type.some((t) => META_TYPES.includes(t)) &&
            current.type.some((t) => !META_TYPES.includes(t))) {
            throw new Error(`Function ${descr.name} has a mix of META and non-META types in the same argument: ${current.type}.`);
        }
        if (current.repeating) {
            if (!consecutiveRepeating && foundRepeating) {
                throw new Error(`Function ${descr.name} has non-consecutive repeating arguments. All repeating arguments must be declared consecutively.`);
            }
            foundRepeating = true;
            consecutiveRepeating = true;
        }
        else {
            consecutiveRepeating = false;
        }
    }
}

const autoCompleteProviders = new Registry();

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

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

// -----------------------------------------------------------------------------
// FORMAT.LARGE.NUMBER
// -----------------------------------------------------------------------------
const FORMAT_LARGE_NUMBER = {
    description: _t("Apply a large number format"),
    args: [
        arg("value (number)", _t("The number.")),
        arg("unit (string, optional)", _t("The formatting unit. Use 'k', 'm', or 'b' to force the unit"), [
            { value: "k", label: _t("Thousand") },
            { value: "m", label: _t("Million") },
            { value: "b", label: _t("Billion") },
        ]),
    ],
    compute: function (value, unite) {
        return {
            value: toNumber(value, this.locale),
            format: formatLargeNumber(value, unite, this.locale),
        };
    },
};

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

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

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

class SubtotalEvaluationPlugin extends UIPlugin {
    subtotalCells = new Set();
    handle(cmd) {
        switch (cmd.type) {
            case "START": {
                this.subtotalCells.clear();
                for (const sheetId of this.getters.getSheetIds()) {
                    const cells = this.getters.getCells(sheetId);
                    for (const cellId in cells) {
                        const cell = cells[cellId];
                        if (isSubtotalCell(cell)) {
                            this.subtotalCells.add(cell.id);
                        }
                    }
                }
                break;
            }
            case "UPDATE_CELL": {
                if (!("content" in cmd))
                    return;
                const cell = this.getters.getCell(cmd);
                if (!cell)
                    return;
                if (isSubtotalCell(cell)) {
                    this.subtotalCells.add(cell.id);
                }
                else {
                    this.subtotalCells.delete(cell.id);
                }
                break;
            }
        }
        if (invalidSubtotalFormulasCommands.has(cmd.type)) {
            this.dispatch("EVALUATE_CELLS", { cellIds: Array.from(this.subtotalCells) });
        }
    }
}
function isSubtotalCell(cell) {
    return (cell.isFormula &&
        cell.compiledFormula.tokens.some((t) => t.type === "SYMBOL" && t.value.toUpperCase() === "SUBTOTAL"));
}

const DEFAULT_FACTOR = 1;
const DEFAULT_MODE = 0;
const DEFAULT_PLACES = 0;
const DEFAULT_SIGNIFICANCE = 1;
const DECIMAL_REPRESENTATION = /^-?[a-z0-9]+$/i;
// -----------------------------------------------------------------------------
// ABS
// -----------------------------------------------------------------------------
const ABS = {
    description: _t("Absolute value of a number."),
    args: [arg("value (number)", _t("The number of which to return the absolute value."))],
    compute: function (value) {
        return Math.abs(toNumber(value, this.locale));
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// ACOS
// -----------------------------------------------------------------------------
const ACOS = {
    description: _t("Inverse cosine of a value, in radians."),
    args: [
        arg("value (number)", _t("The value for which to calculate the inverse cosine. Must be between -1 and 1, inclusive.")),
    ],
    compute: function (value) {
        const _value = toNumber(value, this.locale);
        if (Math.abs(_value) > 1) {
            return new EvaluationError(_t("The value (%s) must be between -1 and 1 inclusive.", _value));
        }
        return Math.acos(_value);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// ACOSH
// -----------------------------------------------------------------------------
const ACOSH = {
    description: _t("Inverse hyperbolic cosine of a number."),
    args: [
        arg("value (number)", _t("The value for which to calculate the inverse hyperbolic cosine. Must be greater than or equal to 1.")),
    ],
    compute: function (value) {
        const _value = toNumber(value, this.locale);
        if (_value < 1) {
            return new EvaluationError(_t("The value (%s) must be greater than or equal to 1.", _value));
        }
        return Math.acosh(_value);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// ACOT
// -----------------------------------------------------------------------------
const ACOT = {
    description: _t("Inverse cotangent of a value."),
    args: [arg("value (number)", _t("The value for which to calculate the inverse cotangent."))],
    compute: function (value) {
        const _value = toNumber(value, this.locale);
        const sign = Math.sign(_value) || 1;
        // ACOT has two possible configurations:
        // @compatibility Excel: return Math.PI / 2 - Math.atan(toNumber(_value, this.locale));
        // @compatibility Google: return sign * Math.PI / 2 - Math.atan(toNumber(_value, this.locale));
        return (sign * Math.PI) / 2 - Math.atan(_value);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// ACOTH
// -----------------------------------------------------------------------------
const ACOTH = {
    description: _t("Inverse hyperbolic cotangent of a value."),
    args: [
        arg("value (number)", _t("The value for which to calculate the inverse hyperbolic cotangent. Must not be between -1 and 1, inclusive.")),
    ],
    compute: function (value) {
        const _value = toNumber(value, this.locale);
        if (Math.abs(_value) <= 1) {
            return new EvaluationError(_t("The value (%s) cannot be between -1 and 1 inclusive.", _value));
        }
        return Math.log((_value + 1) / (_value - 1)) / 2;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// ASIN
// -----------------------------------------------------------------------------
const ASIN = {
    description: _t("Inverse sine of a value, in radians."),
    args: [
        arg("value (number)", _t("The value for which to calculate the inverse sine. Must be between -1 and 1, inclusive.")),
    ],
    compute: function (value) {
        const _value = toNumber(value, this.locale);
        if (Math.abs(_value) > 1) {
            return new EvaluationError(_t("The value (%s) must be between -1 and 1 inclusive.", _value));
        }
        return Math.asin(_value);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// ASINH
// -----------------------------------------------------------------------------
const ASINH = {
    description: _t("Inverse hyperbolic sine of a number."),
    args: [
        arg("value (number)", _t("The value for which to calculate the inverse hyperbolic sine.")),
    ],
    compute: function (value) {
        return Math.asinh(toNumber(value, this.locale));
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// ATAN
// -----------------------------------------------------------------------------
const ATAN = {
    description: _t("Inverse tangent of a value, in radians."),
    args: [arg("value (number)", _t("The value for which to calculate the inverse tangent."))],
    compute: function (value) {
        return Math.atan(toNumber(value, this.locale));
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// ATAN2
// -----------------------------------------------------------------------------
const ATAN2 = {
    description: _t("Angle from the X axis to a point (x,y), in radians."),
    args: [
        arg("x (number)", _t("The x coordinate of the endpoint of the line segment for which to calculate the angle from the x-axis.")),
        arg("y (number)", _t("The y coordinate of the endpoint of the line segment for which to calculate the angle from the x-axis.")),
    ],
    compute: function (x, y) {
        const _x = toNumber(x, this.locale);
        const _y = toNumber(y, this.locale);
        if (_x === 0 && _y === 0) {
            return new DivisionByZeroError(_t("Function [[FUNCTION_NAME]] caused a divide by zero error."));
        }
        return Math.atan2(_y, _x);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// ATANH
// -----------------------------------------------------------------------------
const ATANH = {
    description: _t("Inverse hyperbolic tangent of a number."),
    args: [
        arg("value (number)", _t("The value for which to calculate the inverse hyperbolic tangent. Must be between -1 and 1, exclusive.")),
    ],
    compute: function (value) {
        const _value = toNumber(value, this.locale);
        if (Math.abs(_value) >= 1) {
            return new EvaluationError(_t("The value (%s) must be between -1 and 1 exclusive.", _value));
        }
        return Math.atanh(_value);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// CEILING
// -----------------------------------------------------------------------------
const CEILING = {
    description: _t("Rounds number up to nearest multiple of factor."),
    args: [
        arg("value (number)", _t("The value to round up to the nearest integer multiple of factor.")),
        arg(`factor (number, default=${DEFAULT_FACTOR})`, _t("The number to whose multiples value will be rounded.")),
    ],
    compute: function (value, factor = { value: DEFAULT_FACTOR }) {
        const _value = toNumber(value, this.locale);
        const _factor = toNumber(factor, this.locale);
        if (_factor < 0 && _value > 0) {
            return new EvaluationError(_t("The factor (%s) must be positive when the value (%s) is positive.", _factor, _value));
        }
        return {
            value: _factor ? Math.ceil(_value / _factor) * _factor : 0,
            format: value?.format,
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// CEILING.MATH
// -----------------------------------------------------------------------------
function ceilingMath(number, significance, mode = 0) {
    if (significance === 0) {
        return 0;
    }
    significance = Math.abs(significance);
    if (number >= 0) {
        return Math.ceil(number / significance) * significance;
    }
    if (mode === 0) {
        return -Math.floor(Math.abs(number) / significance) * significance;
    }
    return -Math.ceil(Math.abs(number) / significance) * significance;
}
const CEILING_MATH = {
    description: _t("Rounds number up to nearest multiple of factor."),
    args: [
        arg("number (number)", _t("The value to round up to the nearest integer multiple of significance.")),
        arg(`significance (number, default=${DEFAULT_SIGNIFICANCE})`, _t("The number to whose multiples number will be rounded. The sign of significance will be ignored.")),
        arg(`mode (number, default=${DEFAULT_MODE})`, _t("If number is negative, specifies the rounding direction. If 0 or blank, it is rounded towards zero. Otherwise, it is rounded away from zero.")),
    ],
    compute: function (number, significance = { value: DEFAULT_SIGNIFICANCE }, mode = { value: DEFAULT_MODE }) {
        const _significance = toNumber(significance, this.locale);
        const _number = toNumber(number, this.locale);
        const _mode = toNumber(mode, this.locale);
        return {
            value: ceilingMath(_number, _significance, _mode),
            format: number?.format,
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// CEILING.PRECISE
// -----------------------------------------------------------------------------
const CEILING_PRECISE = {
    description: _t("Rounds number up to nearest multiple of factor."),
    args: [
        arg("number (number)", _t("The value to round up to the nearest integer multiple of significance.")),
        arg(`significance (number, default=${DEFAULT_SIGNIFICANCE})`, _t("The number to whose multiples number will be rounded.")),
    ],
    compute: function (number, significance = { value: DEFAULT_SIGNIFICANCE }) {
        const _significance = toNumber(significance, this.locale);
        const _number = toNumber(number, this.locale);
        return {
            value: ceilingMath(_number, _significance),
            format: number?.format,
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// COS
// -----------------------------------------------------------------------------
const COS = {
    description: _t("Cosine of an angle provided in radians."),
    args: [arg("angle (number)", _t("The angle to find the cosine of, in radians."))],
    compute: function (angle) {
        return Math.cos(toNumber(angle, this.locale));
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// COSH
// -----------------------------------------------------------------------------
const COSH = {
    description: _t("Hyperbolic cosine of any real number."),
    args: [arg("value (number)", _t("Any real value to calculate the hyperbolic cosine of."))],
    compute: function (value) {
        return Math.cosh(toNumber(value, this.locale));
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// COT
// -----------------------------------------------------------------------------
const COT = {
    description: _t("Cotangent of an angle provided in radians."),
    args: [arg("angle (number)", _t("The angle to find the cotangent of, in radians."))],
    compute: function (angle) {
        const _angle = toNumber(angle, this.locale);
        if (_angle === 0) {
            return new DivisionByZeroError(_t("Function [[FUNCTION_NAME]] caused a divide by zero error."));
        }
        return 1 / Math.tan(_angle);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// COTH
// -----------------------------------------------------------------------------
const COTH = {
    description: _t("Hyperbolic cotangent of any real number."),
    args: [arg("value (number)", _t("Any real value to calculate the hyperbolic cotangent of."))],
    compute: function (value) {
        const _value = toNumber(value, this.locale);
        if (_value === 0) {
            return new DivisionByZeroError(_t("Function [[FUNCTION_NAME]] caused a divide by zero error."));
        }
        return 1 / Math.tanh(_value);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// COUNTBLANK
// -----------------------------------------------------------------------------
const COUNTBLANK = {
    description: _t("Number of empty values."),
    args: [
        arg("value1 (any, range)", _t("The first value or range in which to count the number of blanks.")),
        arg("value2 (any, range, repeating)", _t("Additional values or ranges in which to count the number of blanks.")),
    ],
    compute: function (...args) {
        return reduceAny(args, (acc, a) => {
            if (a === undefined) {
                return acc + 1;
            }
            if (a.value === null) {
                return acc + 1;
            }
            if (a.value === "") {
                return acc + 1;
            }
            return acc;
        }, 0);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// COUNTIF
// -----------------------------------------------------------------------------
const COUNTIF = {
    description: _t("A conditional count across a range."),
    args: [
        arg("range (range)", _t("The range that is tested against criterion.")),
        arg("criterion (string)", _t("The pattern or test to apply to range.")),
    ],
    compute: function (...args) {
        let count = 0;
        visitMatchingRanges(args, (i, j) => {
            count += 1;
        }, this.locale);
        return count;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// COUNTIFS
// -----------------------------------------------------------------------------
const COUNTIFS = {
    description: _t("Count values depending on multiple criteria."),
    args: [
        arg("criteria_range1 (range)", _t("The range to check against criterion1.")),
        arg("criterion1 (string)", _t("The pattern or test to apply to criteria_range1.")),
        arg("criteria_range2 (any, range, repeating)", _t("Additional ranges over which to evaluate the additional criteria. The filtered set will be the intersection of the sets produced by each criterion-range pair.")),
        arg("criterion2 (string, repeating)", _t("Additional criteria to check.")),
    ],
    compute: function (...args) {
        let count = 0;
        visitMatchingRanges(args, (i, j) => {
            count += 1;
        }, this.locale);
        return count;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// COUNTUNIQUE
// -----------------------------------------------------------------------------
const COUNTUNIQUE = {
    description: _t("Counts number of unique values in a range."),
    args: [
        arg("value1 (any, range)", _t("The first value or range to consider for uniqueness.")),
        arg("value2 (any, range, repeating)", _t("Additional values or ranges to consider for uniqueness.")),
    ],
    compute: function (...args) {
        return countUnique(args);
    },
};
// -----------------------------------------------------------------------------
// COUNTUNIQUEIFS
// -----------------------------------------------------------------------------
const COUNTUNIQUEIFS = {
    description: _t("Counts number of unique values in a range, filtered by a set of criteria."),
    args: [
        arg("range (range)", _t("The range of cells from which the number of unique values will be counted.")),
        arg("criteria_range1 (range)", _t("The range of cells over which to evaluate criterion1.")),
        arg("criterion1 (string)", _t("The pattern or test to apply to criteria_range1, such that each cell that evaluates to TRUE will be included in the filtered set.")),
        arg("criteria_range2 (any, range, repeating)", _t("Additional ranges over which to evaluate the additional criteria. The filtered set will be the intersection of the sets produced by each criterion-range pair.")),
        arg("criterion2 (string, repeating)", _t("The pattern or test to apply to criteria_range2.")),
    ],
    compute: function (range, ...args) {
        const uniqueValues = new Set();
        visitMatchingRanges(args, (i, j) => {
            const data = range[i]?.[j];
            if (isDataNonEmpty(data)) {
                uniqueValues.add(data.value);
            }
        }, this.locale);
        return uniqueValues.size;
    },
};
// -----------------------------------------------------------------------------
// CSC
// -----------------------------------------------------------------------------
const CSC = {
    description: _t("Cosecant of an angle provided in radians."),
    args: [arg("angle (number)", _t("The angle to find the cosecant of, in radians."))],
    compute: function (angle) {
        const _angle = toNumber(angle, this.locale);
        if (_angle === 0) {
            return new DivisionByZeroError(_t("Function [[FUNCTION_NAME]] caused a divide by zero error."));
        }
        return 1 / Math.sin(_angle);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// CSCH
// -----------------------------------------------------------------------------
const CSCH = {
    description: _t("Hyperbolic cosecant of any real number."),
    args: [arg("value (number)", _t("Any real value to calculate the hyperbolic cosecant of."))],
    compute: function (value) {
        const _value = toNumber(value, this.locale);
        if (_value === 0) {
            return new DivisionByZeroError(_t("Function [[FUNCTION_NAME]] caused a divide by zero error."));
        }
        return 1 / Math.sinh(_value);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// DECIMAL
// -----------------------------------------------------------------------------
const DECIMAL = {
    description: _t("Converts from another base to decimal."),
    args: [
        arg("value (string)", _t("The number to convert.")),
        arg("base (number)", _t("The base to convert the value from.")),
    ],
    compute: function (value, base) {
        let _base = toNumber(base, this.locale);
        _base = Math.floor(_base);
        if (2 > _base || _base > 36) {
            return new EvaluationError(_t("The base (%s) must be between 2 and 36 inclusive.", _base));
        }
        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.
         */
        if (!DECIMAL_REPRESENTATION.test(_value)) {
            return new EvaluationError(_t("The value (%s) must be a valid base %s representation.", _value, _base));
        }
        const deci = parseInt(_value, _base);
        if (isNaN(deci)) {
            return new EvaluationError(_t("The value (%s) must be a valid base %s representation.", _value, _base));
        }
        return deci;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// DEGREES
// -----------------------------------------------------------------------------
const DEGREES = {
    description: _t("Converts an angle value in radians to degrees."),
    args: [arg("angle (number)", _t("The angle to convert from radians to degrees."))],
    compute: function (angle) {
        return (toNumber(angle, this.locale) * 180) / Math.PI;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// EXP
// -----------------------------------------------------------------------------
const EXP = {
    description: _t("Euler's number, e (~2.718) raised to a power."),
    args: [arg("value (number)", _t("The exponent to raise e."))],
    compute: function (value) {
        return Math.exp(toNumber(value, this.locale));
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// FLOOR
// -----------------------------------------------------------------------------
const FLOOR = {
    description: _t("Rounds number down to nearest multiple of factor."),
    args: [
        arg("value (number)", _t("The value to round down to the nearest integer multiple of factor.")),
        arg(`factor (number, default=${DEFAULT_FACTOR})`, _t("The number to whose multiples value will be rounded.")),
    ],
    compute: function (value, factor = { value: DEFAULT_FACTOR }) {
        const _value = toNumber(value, this.locale);
        const _factor = toNumber(factor, this.locale);
        if (_factor < 0 && _value > 0) {
            return new EvaluationError(_t("The factor (%s) must be positive when the value (%s) is positive.", _factor, _value));
        }
        return {
            value: _factor ? Math.floor(_value / _factor) * _factor : 0,
            format: value?.format,
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// FLOOR.MATH
// -----------------------------------------------------------------------------
function floorMath(number, significance, mode = 0) {
    if (significance === 0) {
        return 0;
    }
    significance = Math.abs(significance);
    if (number >= 0) {
        return Math.floor(number / significance) * significance;
    }
    if (mode === 0) {
        return -Math.ceil(Math.abs(number) / significance) * significance;
    }
    return -Math.floor(Math.abs(number) / significance) * significance;
}
const FLOOR_MATH = {
    description: _t("Rounds number down to nearest multiple of factor."),
    args: [
        arg("number (number)", _t("The value to round down to the nearest integer multiple of significance.")),
        arg(`significance (number, default=${DEFAULT_SIGNIFICANCE})`, _t("The number to whose multiples number will be rounded. The sign of significance will be ignored.")),
        arg(`mode (number, default=${DEFAULT_MODE})`, _t("If number is negative, specifies the rounding direction. If 0 or blank, it is rounded away from zero. Otherwise, it is rounded towards zero.")),
    ],
    compute: function (number, significance = { value: DEFAULT_SIGNIFICANCE }, mode = { value: DEFAULT_MODE }) {
        const _significance = toNumber(significance, this.locale);
        const _number = toNumber(number, this.locale);
        const _mode = toNumber(mode, this.locale);
        return {
            value: floorMath(_number, _significance, _mode),
            format: number?.format,
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// FLOOR.PRECISE
// -----------------------------------------------------------------------------
const FLOOR_PRECISE = {
    description: _t("Rounds number down to nearest multiple of factor."),
    args: [
        arg("number (number)", _t("The value to round down to the nearest integer multiple of significance.")),
        arg(`significance (number, default=${DEFAULT_SIGNIFICANCE})`, _t("The number to whose multiples number will be rounded.")),
    ],
    compute: function (number, significance = { value: DEFAULT_SIGNIFICANCE }) {
        const _significance = toNumber(significance, this.locale);
        const _number = toNumber(number, this.locale);
        return {
            value: floorMath(_number, _significance),
            format: number?.format,
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// ISEVEN
// -----------------------------------------------------------------------------
const ISEVEN = {
    description: _t("Whether the provided value is even."),
    args: [arg("value (number)", _t("The value to be verified as even."))],
    compute: function (value) {
        const _value = strictToNumber(value, this.locale);
        return Math.floor(Math.abs(_value)) & 1 ? false : true;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// ISO.CEILING
// -----------------------------------------------------------------------------
const ISO_CEILING = {
    description: _t("Rounds number up to nearest multiple of factor."),
    args: [
        arg("number (number)", _t("The value to round up to the nearest integer multiple of significance.")),
        arg(`significance (number, default=${DEFAULT_SIGNIFICANCE})`, _t("The number to whose multiples number will be rounded.")),
    ],
    compute: function (number, significance = { value: DEFAULT_SIGNIFICANCE }) {
        const _number = toNumber(number, this.locale);
        const _significance = toNumber(significance, this.locale);
        return {
            value: ceilingMath(_number, _significance),
            format: number?.format,
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// ISODD
// -----------------------------------------------------------------------------
const ISODD = {
    description: _t("Whether the provided value is even."),
    args: [arg("value (number)", _t("The value to be verified as even."))],
    compute: function (value) {
        const _value = strictToNumber(value, this.locale);
        return Math.floor(Math.abs(_value)) & 1 ? true : false;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// LN
// -----------------------------------------------------------------------------
const LN = {
    description: _t("The logarithm of a number, base e (euler's number)."),
    args: [arg("value (number)", _t("The value for which to calculate the logarithm, base e."))],
    compute: function (value) {
        const _value = toNumber(value, this.locale);
        if (_value <= 0) {
            return new EvaluationError(_t("The value (%s) must be strictly positive.", _value));
        }
        return Math.log(_value);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// LOG
// -----------------------------------------------------------------------------
const LOG = {
    description: _t("The logarithm of a number, for a given base."),
    args: [
        arg("value (number)", _t("The value for which to calculate the logarithm.")),
        arg("base (number, default=10)", _t("The base of the logarithm.")),
    ],
    compute: function (value, base = { value: 10 }) {
        const _value = toNumber(value, this.locale);
        const _base = toNumber(base, this.locale);
        if (_value <= 0) {
            return new EvaluationError(_t("The value (%s) must be strictly positive.", _value));
        }
        if (_base <= 0) {
            return new EvaluationError(_t("The base (%s) must be strictly positive.", _base));
        }
        if (_base === 1) {
            return new EvaluationError(_t("The base must be different from 1."));
        }
        return Math.log10(_value) / Math.log10(_base);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// MOD
// -----------------------------------------------------------------------------
function mod(dividend, divisor) {
    assertNotZero(divisor, _t("The divisor must be different from 0."));
    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;
}
const MOD = {
    description: _t("Modulo (remainder) operator."),
    args: [
        arg("dividend (number)", _t("The number to be divided to find the remainder.")),
        arg("divisor (number)", _t("The number to divide by.")),
    ],
    compute: function (dividend, divisor) {
        const _divisor = toNumber(divisor, this.locale);
        const _dividend = toNumber(dividend, this.locale);
        return {
            value: mod(_dividend, _divisor),
            format: dividend?.format,
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// MUNIT
// -----------------------------------------------------------------------------
const MUNIT = {
    description: _t("Returns a n x n unit matrix, where n is the input dimension."),
    args: [
        arg("dimension (number)", _t("An integer specifying the dimension size of the unit matrix. It must be positive.")),
    ],
    compute: function (n) {
        const _n = toInteger(n, this.locale);
        if (_n < 1) {
            return new EvaluationError(_t("The argument dimension must be positive"));
        }
        return getUnitMatrix(_n);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// ODD
// -----------------------------------------------------------------------------
const ODD = {
    description: _t("Rounds a number up to the nearest odd integer."),
    args: [arg("value (number)", _t("The value to round to the next greatest odd number."))],
    compute: function (value) {
        const _value = toNumber(value, this.locale);
        let temp = Math.ceil(Math.abs(_value));
        temp = temp & 1 ? temp : temp + 1;
        return {
            value: _value < 0 ? -temp : temp,
            format: value?.format,
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// PI
// -----------------------------------------------------------------------------
const PI = {
    description: _t("The number pi."),
    args: [],
    compute: function () {
        return Math.PI;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// POWER
// -----------------------------------------------------------------------------
const POWER = {
    description: _t("A number raised to a power."),
    args: [
        arg("base (number)", _t("The number to raise to the exponent power.")),
        arg("exponent (number)", _t("The exponent to raise base to.")),
    ],
    compute: function (base, exponent) {
        const _base = toNumber(base, this.locale);
        const _exponent = toNumber(exponent, this.locale);
        if (_base < 0 && !Number.isInteger(_exponent)) {
            return new EvaluationError(_t("The exponent (%s) must be an integer when the base is negative.", _exponent));
        }
        return { value: Math.pow(_base, _exponent), format: base?.format };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// PRODUCT
// -----------------------------------------------------------------------------
const PRODUCT = {
    description: _t("Result of multiplying a series of numbers together."),
    args: [
        arg("factor1 (number, range<number>)", _t("The first number or range to calculate for the product.")),
        arg("factor2 (number, range<number>, repeating)", _t("More numbers or ranges to calculate for the product.")),
    ],
    compute: function (...factors) {
        let count = 0;
        let acc = 1;
        for (const n of factors) {
            if (isMatrix(n)) {
                for (const i of n) {
                    for (const j of i) {
                        const f = j.value;
                        if (typeof f === "number") {
                            acc *= f;
                            count += 1;
                        }
                        if (isEvaluationError(f)) {
                            return j;
                        }
                    }
                }
            }
            else if (n !== undefined && n.value !== null) {
                acc *= strictToNumber(n, this.locale);
                count += 1;
            }
        }
        return {
            value: count === 0 ? 0 : acc,
            format: inferFormat(factors[0]),
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// RAND
// -----------------------------------------------------------------------------
const RAND = {
    description: _t("A random number between 0 inclusive and 1 exclusive."),
    args: [],
    compute: function () {
        return Math.random();
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// RANDARRAY
// -----------------------------------------------------------------------------
const RANDARRAY = {
    description: _t("Returns a grid of random numbers between 0 inclusive and 1 exclusive."),
    args: [
        arg("rows (number, default=1)", _t("The number of rows to be returned.")),
        arg("columns (number, default=1)", _t("The number of columns to be returned.")),
        arg("min (number, default=0)", _t("The minimum number you would like returned.")),
        arg("max (number, default=1)", _t("The maximum number you would like returned.")),
        arg("whole_number (boolean, default=FALSE)", _t("Return a whole number or a decimal value."), [
            { value: false, label: _t("Decimal (default)") },
            { value: true, label: _t("Integer") },
        ]),
    ],
    compute: function (rows = { value: 1 }, columns = { value: 1 }, min = { value: 0 }, max = { value: 1 }, wholeNumber = { value: false }) {
        const _cols = toInteger(columns, this.locale);
        const _rows = toInteger(rows, this.locale);
        const _min = toNumber(min, this.locale);
        const _max = toNumber(max, this.locale);
        const _whole_number = toBoolean(wholeNumber);
        if (_cols < 1) {
            return new EvaluationError(_t("The number of columns (%s) must be positive.", _cols));
        }
        if (_rows < 1) {
            return new EvaluationError(_t("The number of rows (%s) must be positive.", _rows));
        }
        if (_min > _max) {
            return new EvaluationError(_t("The maximum (%s) must be greater than or equal to the minimum (%s).", _max, _min));
        }
        if (_whole_number) {
            if (!Number.isInteger(_min) || !Number.isInteger(_max)) {
                return new EvaluationError(_t("The maximum (%s) and minimum (%s) must be integers when whole_number is TRUE.", _max.toString(), _min.toString()));
            }
        }
        const result = Array(_cols);
        for (let col = 0; col < _cols; col++) {
            result[col] = Array(_rows);
            for (let row = 0; row < _rows; row++) {
                if (!_whole_number) {
                    result[col][row] = _min + Math.random() * (_max - _min);
                }
                else {
                    result[col][row] = Math.floor(Math.random() * (_max - _min + 1) + _min);
                }
            }
        }
        return result;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// RANDBETWEEN
// -----------------------------------------------------------------------------
const RANDBETWEEN = {
    description: _t("Random integer between two values, inclusive."),
    args: [
        arg("low (number)", _t("The low end of the random range.")),
        arg("high (number)", _t("The high end of the random range.")),
    ],
    compute: function (low, high) {
        let _low = toNumber(low, this.locale);
        if (!Number.isInteger(_low)) {
            _low = Math.ceil(_low);
        }
        let _high = toNumber(high, this.locale);
        if (!Number.isInteger(_high)) {
            _high = Math.floor(_high);
        }
        if (_low > _high) {
            return new EvaluationError(_t("The high (%s) must be greater than or equal to the low (%s).", _high, _low));
        }
        return {
            value: _low + Math.ceil((_high - _low + 1) * Math.random()) - 1,
            format: low?.format,
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// ROUND
// -----------------------------------------------------------------------------
const ROUND = {
    description: _t("Rounds a number according to standard rules."),
    args: [
        arg("value (number)", _t("The value to round to places number of places.")),
        arg(`places (number, default=${DEFAULT_PLACES})`, _t("The number of decimal places to which to round.")),
    ],
    compute: function (value, places = { value: DEFAULT_PLACES }) {
        const _value = toNumber(value, this.locale);
        let _places = toNumber(places, this.locale);
        const absValue = Math.abs(_value);
        let tempResult;
        if (_places === 0) {
            tempResult = Math.round(absValue);
        }
        else {
            if (!Number.isInteger(_places)) {
                _places = Math.trunc(_places);
            }
            tempResult = Math.round(absValue * Math.pow(10, _places)) / Math.pow(10, _places);
        }
        return {
            value: _value >= 0 ? tempResult : -tempResult,
            format: value?.format,
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// ROUNDDOWN
// -----------------------------------------------------------------------------
const ROUNDDOWN = {
    description: _t("Rounds down a number."),
    args: [
        arg("value (number)", _t("The value to round to places number of places, always rounding down.")),
        arg(`places (number, default=${DEFAULT_PLACES})`, _t("The number of decimal places to which to round.")),
    ],
    compute: function (value, places = { value: DEFAULT_PLACES }) {
        const _value = toNumber(value, this.locale);
        let _places = toNumber(places, this.locale);
        const absValue = Math.abs(_value);
        let tempResult;
        if (_places === 0) {
            tempResult = Math.floor(absValue);
        }
        else {
            if (!Number.isInteger(_places)) {
                _places = Math.trunc(_places);
            }
            tempResult = Math.floor(absValue * Math.pow(10, _places)) / Math.pow(10, _places);
        }
        return {
            value: _value >= 0 ? tempResult : -tempResult,
            format: value?.format,
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// ROUNDUP
// -----------------------------------------------------------------------------
const ROUNDUP = {
    description: _t("Rounds up a number."),
    args: [
        arg("value (number)", _t("The value to round to places number of places, always rounding up.")),
        arg(`places (number, default=${DEFAULT_PLACES})`, _t("The number of decimal places to which to round.")),
    ],
    compute: function (value, places = { value: DEFAULT_PLACES }) {
        const _value = toNumber(value, this.locale);
        let _places = toNumber(places, this.locale);
        const absValue = Math.abs(_value);
        let tempResult;
        if (_places === 0) {
            tempResult = Math.ceil(absValue);
        }
        else {
            if (!Number.isInteger(_places)) {
                _places = Math.trunc(_places);
            }
            tempResult = Math.ceil(absValue * Math.pow(10, _places)) / Math.pow(10, _places);
        }
        return {
            value: _value >= 0 ? tempResult : -tempResult,
            format: value?.format,
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// SEC
// -----------------------------------------------------------------------------
const SEC = {
    description: _t("Secant of an angle provided in radians."),
    args: [arg("angle (number)", _t("The angle to find the secant of, in radians."))],
    compute: function (angle) {
        return 1 / Math.cos(toNumber(angle, this.locale));
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// SECH
// -----------------------------------------------------------------------------
const SECH = {
    description: _t("Hyperbolic secant of any real number."),
    args: [arg("value (number)", _t("Any real value to calculate the hyperbolic secant of."))],
    compute: function (value) {
        return 1 / Math.cosh(toNumber(value, this.locale));
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// SEQUENCE
// -----------------------------------------------------------------------------
const SEQUENCE = {
    description: _t("Returns a sequence of numbers."),
    args: [
        arg("rows (number)", _t("The number of rows to return")),
        arg("columns (number, optional, default=1)", _t("The number of columns to return")),
        arg("start (number, optional, default=1)", _t("The first number in the sequence")),
        arg("step (number, optional, default=1)", _t("The amount to increment each value in the sequence")),
    ],
    compute: function (rows, columns = { value: 1 }, start = { value: 1 }, step = { value: 1 }) {
        const _start = toNumber(start, this.locale);
        const _step = toNumber(step, this.locale);
        const _rows = toInteger(rows, this.locale);
        const _columns = toInteger(columns, this.locale);
        if (_columns < 1) {
            return new EvaluationError(_t("The number of columns (%s) must be positive.", _columns));
        }
        if (_rows < 1) {
            return new EvaluationError(_t("The number of rows (%s) must be positive.", _rows));
        }
        return generateMatrix(_columns, _rows, (col, row) => {
            return {
                value: _start + row * _columns * _step + col * _step,
            };
        });
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// SIN
// -----------------------------------------------------------------------------
const SIN = {
    description: _t("Sine of an angle provided in radians."),
    args: [arg("angle (number)", _t("The angle to find the sine of, in radians."))],
    compute: function (angle) {
        return Math.sin(toNumber(angle, this.locale));
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// SINH
// -----------------------------------------------------------------------------
const SINH = {
    description: _t("Hyperbolic sine of any real number."),
    args: [arg("value (number)", _t("Any real value to calculate the hyperbolic sine of."))],
    compute: function (value) {
        return Math.sinh(toNumber(value, this.locale));
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// SQRT
// -----------------------------------------------------------------------------
const SQRT = {
    description: _t("Positive square root of a positive number."),
    args: [arg("value (number)", _t("The number for which to calculate the positive square root."))],
    compute: function (value) {
        const _value = toNumber(value, this.locale);
        if (_value < 0) {
            return new EvaluationError(_t("The value (%s) must be positive or null.", _value));
        }
        return { value: Math.sqrt(_value), format: value?.format };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// SUBTOTAL
// -----------------------------------------------------------------------------
const subtotalFunctionAggregateByCode = {
    1: "AVERAGE",
    2: "COUNT",
    3: "COUNTA",
    4: "MAX",
    5: "MIN",
    6: "PRODUCT",
    7: "STDEV",
    8: "STDEVP",
    9: "SUM",
    10: "VAR",
    11: "VARP",
};
const subtotalFunctionOptionsIncludeHiddenRows = Object.entries(subtotalFunctionAggregateByCode).map(([number, functionName]) => ({
    value: parseInt(number),
    label: _t("%s (include manually-hidden rows)", functionName),
}));
const subtotalFunctionOptionsExcludeHiddenRows = Object.entries(subtotalFunctionAggregateByCode).map(([number, functionName]) => ({
    value: parseInt(number) + 100,
    label: _t("%s (exclude manually-hidden rows)", functionName),
}));
const SUBTOTAL = {
    description: _t("Returns a subtotal for a vertical range of cells using a specified aggregation function."),
    args: [
        arg("function_code (number)", _t("The function to use in subtotal aggregation."), [
            ...subtotalFunctionOptionsIncludeHiddenRows,
            ...subtotalFunctionOptionsExcludeHiddenRows,
        ]),
        arg("ref1 (meta, range<meta>)", _t("The range or reference for which you want the subtotal.")),
        arg("ref2 (meta, range<meta>, repeating)", _t("Additional ranges or references for which you want the subtotal.")),
    ],
    compute: function (functionCode, ...refs) {
        let code = toInteger(functionCode, this.locale);
        let acceptHiddenCells = true;
        if (code > 100) {
            code -= 100;
            acceptHiddenCells = false;
        }
        if (code < 1 || code > 11) {
            return new EvaluationError(_t("The function code (%s) must be between 1 to 11 or 101 to 111.", code));
        }
        const evaluatedCellToKeep = [];
        for (const ref of refs) {
            const ref0 = ref[0][0];
            const sheetName = splitReference(ref0.value).sheetName;
            const sheetId = sheetName ? this.getters.getSheetIdByName(sheetName) : this.__originSheetId;
            if (!sheetId)
                continue;
            const { top, left } = toZone(ref0.value);
            const right = left + ref.length - 1;
            const bottom = top + ref[0].length - 1;
            for (let row = top; row <= bottom; row++) {
                if (this.getters.isRowFiltered(sheetId, row))
                    continue;
                if (!acceptHiddenCells && this.getters.isRowHiddenByUser(sheetId, row))
                    continue;
                for (let col = left; col <= right; col++) {
                    const cell = this.getters.getCorrespondingFormulaCell({ sheetId, col, row });
                    if (!cell || !isSubtotalCell(cell)) {
                        evaluatedCellToKeep.push(this.getters.getEvaluatedCell({ sheetId, col, row }));
                    }
                }
            }
        }
        return this[subtotalFunctionAggregateByCode[code]].apply(this, [[evaluatedCellToKeep]]);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// SUM
// -----------------------------------------------------------------------------
const SUM = {
    description: _t("Sum of a series of numbers and/or cells."),
    args: [
        arg("value1 (number, range<number>)", _t("The first number or range to add together.")),
        arg("value2 (number, range<number>, repeating)", _t("Additional numbers or ranges to add to value1.")),
    ],
    compute: function (...values) {
        const v1 = values[0];
        return {
            value: sum(values, this.locale),
            format: inferFormat(v1),
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// SUMIF
// -----------------------------------------------------------------------------
const SUMIF = {
    description: _t("A conditional sum across a range."),
    args: [
        arg("criteria_range (range)", _t("The range which is tested against criterion.")),
        arg("criterion (string)", _t("The pattern or test to apply to range.")),
        arg("sum_range (range, default=criteria_range)", _t("The range to be summed, if different from range.")),
    ],
    compute: function (criteriaRange, criterion, sumRange) {
        if (sumRange === undefined) {
            sumRange = criteriaRange;
        }
        let sum = 0;
        visitMatchingRanges([criteriaRange, criterion], (i, j) => {
            const value = sumRange[i]?.[j]?.value;
            if (typeof value === "number") {
                sum += value;
            }
        }, this.locale);
        return sum;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// SUMIFS
// -----------------------------------------------------------------------------
const SUMIFS = {
    description: _t("Sums a range depending on multiple criteria."),
    args: [
        arg("sum_range (range)", _t("The range to sum.")),
        arg("criteria_range1 (range)", _t("The range to check against criterion1.")),
        arg("criterion1 (string)", _t("The pattern or test to apply to criteria_range1.")),
        arg("criteria_range2 (any, range, repeating)", _t("Additional ranges to check.")),
        arg("criterion2 (string, repeating)", _t("Additional criteria to check.")),
    ],
    compute: function (sumRange, ...criters) {
        let sum = 0;
        visitMatchingRanges(criters, (i, j) => {
            const value = sumRange[i]?.[j]?.value;
            if (typeof value === "number") {
                sum += value;
            }
        }, this.locale);
        return sum;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// TAN
// -----------------------------------------------------------------------------
const TAN = {
    description: _t("Tangent of an angle provided in radians."),
    args: [arg("angle (number)", _t("The angle to find the tangent of, in radians."))],
    compute: function (angle) {
        return Math.tan(toNumber(angle, this.locale));
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// TANH
// -----------------------------------------------------------------------------
const TANH = {
    description: _t("Hyperbolic tangent of any real number."),
    args: [arg("value (number)", _t("Any real value to calculate the hyperbolic tangent of."))],
    compute: function (value) {
        return Math.tanh(toNumber(value, this.locale));
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// TRUNC
// -----------------------------------------------------------------------------
function trunc(value, 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);
}
const TRUNC = {
    description: _t("Truncates a number."),
    args: [
        arg("value (number)", _t("The value to be truncated.")),
        arg(`places (number, default=${DEFAULT_PLACES})`, _t("The number of significant digits to the right of the decimal point to retain.")),
    ],
    compute: function (value, places = { value: DEFAULT_PLACES }) {
        const _value = toNumber(value, this.locale);
        const _places = toNumber(places, this.locale);
        return { value: trunc(_value, _places), format: value?.format };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// INT
// -----------------------------------------------------------------------------
const INT = {
    description: _t("Rounds a number down to the nearest integer that is less than or equal to it."),
    args: [arg("value (number)", _t("The number to round down to the nearest integer."))],
    compute: function (value) {
        return Math.floor(toNumber(value, this.locale));
    },
    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,
    INT: INT,
    ISEVEN: ISEVEN,
    ISODD: ISODD,
    ISO_CEILING: ISO_CEILING,
    LN: LN,
    LOG: LOG,
    MOD: MOD,
    MUNIT: MUNIT,
    ODD: ODD,
    PI: PI,
    POWER: POWER,
    PRODUCT: PRODUCT,
    RAND: RAND,
    RANDARRAY: RANDARRAY,
    RANDBETWEEN: RANDBETWEEN,
    ROUND: ROUND,
    ROUNDDOWN: ROUNDDOWN,
    ROUNDUP: ROUNDUP,
    SEC: SEC,
    SECH: SECH,
    SEQUENCE: SEQUENCE,
    SIN: SIN,
    SINH: SINH,
    SQRT: SQRT,
    SUBTOTAL: SUBTOTAL,
    SUM: SUM,
    SUMIF: SUMIF,
    SUMIFS: SUMIFS,
    TAN: TAN,
    TANH: TANH,
    TRUNC: TRUNC
});

const CALCULATE_B_OPTIONS = [
    { value: true, label: _t("b is calculated normally") },
    { value: false, label: _t("b is forced to 1") },
];
const RETURN_VERBOSE_OPTIONS = [
    { value: false, label: _t("do not return additional regression statistics") },
    { value: true, label: _t("return additional regression statistics") },
];
const POLYNOMIAL_ORDER_OPTIONS = [
    { value: 1, label: _t("order 1 (Linear)") },
    { value: 2, label: _t("order 2 (Quadratic)") },
    { value: 3, label: _t("order 3 (Cubic)") },
    { value: 4, label: _t("order 4 (Quartic)") },
    { value: 5, label: _t("order 5 (Quintic)") },
    { value: 6, label: _t("order 6 (Sextic)") },
];
const COMPUTE_INTERCEPT_OPTIONS = [
    { value: true, label: _t("Compute intercept") },
    { value: false, label: _t("Force intercept to 0") },
];
const QUARTILE_NUMBER_OPTIONS = [
    { value: 0, label: _t("Minimum value") },
    { value: 1, label: _t("First quartile (25th percentile)") },
    { value: 2, label: _t("Median value (50th percentile)") },
    { value: 3, label: _t("Third quartile (75th percentile)") },
    { value: 4, label: _t("Maximum value") },
];
function filterAndFlatData(dataY, dataX) {
    const _flatDataY = [];
    const _flatDataX = [];
    let lenY = 0;
    let lenX = 0;
    visitAny([dataY], (y) => {
        _flatDataY.push(y);
        lenY += 1;
    });
    visitAny([dataX], (x) => {
        _flatDataX.push(x);
        lenX += 1;
    });
    assert(lenY === lenX, _t("[[FUNCTION_NAME]] has mismatched argument count %s vs %s.", lenY, lenX));
    const flatDataX = [];
    const flatDataY = [];
    for (let i = 0; i < lenY; i++) {
        const valueY = _flatDataY[i]?.value;
        const valueX = _flatDataX[i]?.value;
        if (typeof valueY === "number" && typeof valueX === "number") {
            flatDataY.push(valueY);
            flatDataX.push(valueX);
        }
    }
    return { flatDataX, flatDataY };
}
// Note: dataY and dataX may not have the same dimension
function covariance(dataY, dataX, isSample) {
    const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX);
    const count = flatDataY.length;
    assertNotZero(count);
    if (isSample) {
        assertNotZero(count - 1);
    }
    let sumY = 0;
    let sumX = 0;
    for (let i = 0; i < count; i++) {
        sumY += flatDataY[i];
        sumX += flatDataX[i];
    }
    const averageY = sumY / count;
    const averageX = sumX / count;
    let acc = 0;
    for (let i = 0; i < count; i++) {
        acc += (flatDataY[i] - averageY) * (flatDataX[i] - averageX);
    }
    return acc / (count - (isSample ? 1 : 0));
}
function variance(args, isSample, textAs0, locale) {
    let count = 0;
    const reduceFunction = textAs0 ? reduceNumbersTextAs0 : reduceNumbers;
    const sum = reduceFunction(args, (acc, a) => {
        count += 1;
        return acc + a;
    }, 0, locale);
    assertNotZero(count);
    if (isSample) {
        assertNotZero(count - 1);
    }
    const average = sum / count;
    return (reduceFunction(args, (acc, a) => acc + Math.pow(a - average, 2), 0, locale) /
        (count - (isSample ? 1 : 0)));
}
function centile(data, percent, isInclusive, locale) {
    const _percent = toNumber(percent, locale);
    assert(isInclusive ? 0 <= _percent && _percent <= 1 : 0 < _percent && _percent < 1, _t("Function [[FUNCTION_NAME]] parameter 2 value is out of range."));
    const sortedArray = [];
    let index;
    let count = 0;
    visitAny(data, (d) => {
        const value = d?.value;
        if (typeof value === "number") {
            index = dichotomicSearch(sortedArray, d, "nextSmaller", "asc", sortedArray.length, (array, i) => array[i]);
            sortedArray.splice(index + 1, 0, value);
            count++;
        }
    });
    assert(count !== 0, noValidInputErrorMessage);
    if (!isInclusive) {
        // 2nd argument must be between 1/(n+1) and n/(n+1) with n the number of data
        assert(1 / (count + 1) <= _percent && _percent <= count / (count + 1), _t("Function [[FUNCTION_NAME]] parameter 2 value is out of range."));
    }
    return percentile(sortedArray, _percent, isInclusive);
}
// -----------------------------------------------------------------------------
// AVEDEV
// -----------------------------------------------------------------------------
const AVEDEV = {
    description: _t("Average magnitude of deviations from mean."),
    args: [
        arg("value1 (number, range<number>)", _t("The first value or range of the sample.")),
        arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to include in the sample.")),
    ],
    compute: function (...values) {
        let count = 0;
        const sum = reduceNumbers(values, (acc, a) => {
            count += 1;
            return acc + a;
        }, 0, this.locale);
        if (count === 0) {
            return new DivisionByZeroError(_t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
        }
        const average = sum / count;
        return reduceNumbers(values, (acc, a) => acc + Math.abs(average - a), 0, this.locale) / count;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// AVERAGE
// -----------------------------------------------------------------------------
const AVERAGE = {
    description: _t("Numerical average value in a dataset, ignoring text."),
    args: [
        arg("value1 (number, range<number>)", _t("The first value or range to consider when calculating the average value.")),
        arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to consider when calculating the average value.")),
    ],
    compute: function (...values) {
        return {
            value: average(values, this.locale),
            format: inferFormat(values[0]),
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// AVERAGE.WEIGHTED
// -----------------------------------------------------------------------------
const rangeError = _t("[[FUNCTION_NAME]] has mismatched range sizes.");
const negativeWeightError = _t("[[FUNCTION_NAME]] expects the weight to be positive or equal to 0.");
const AVERAGE_WEIGHTED = {
    description: _t("Weighted average."),
    args: [
        arg("values (number, range<number>)", _t("Values to average.")),
        arg("weights (number, range<number>)", _t("Weights for each corresponding value.")),
        arg("additional_values (number, range<number>, repeating)", _t("Additional values to average.")),
        arg("additional_weights (number, range<number>, repeating)", _t("Additional weights.")),
    ],
    compute: function (...args) {
        let sum = 0;
        let count = 0;
        for (let n = 0; n < args.length - 1; n += 2) {
            const argN = args[n];
            const argN1 = args[n + 1];
            if (!areSameDimensions(argN, argN1)) {
                return new EvaluationError(rangeError);
            }
            if (isMatrix(argN)) {
                for (let i = 0; i < argN.length; i++) {
                    for (let j = 0; j < argN[0].length; j++) {
                        const value = argN[i][j].value;
                        const weight = isMatrix(argN1) ? argN1?.[i][j].value : toNumber(argN1, this.locale);
                        const valueIsNumber = typeof value === "number";
                        const weightIsNumber = typeof weight === "number";
                        if (valueIsNumber && weightIsNumber) {
                            if (weight < 0) {
                                return new EvaluationError(negativeWeightError);
                            }
                            sum += value * weight;
                            count += weight;
                            continue;
                        }
                        if (valueIsNumber !== weightIsNumber) {
                            return new EvaluationError(_t("[[FUNCTION_NAME]] expects number values."));
                        }
                    }
                }
            }
            else {
                const value = toNumber(argN, this.locale);
                const weight = isMatrix(argN1) ? argN1?.[0][0].value : toNumber(argN1, this.locale);
                if (typeof weight === "number") {
                    if (weight < 0) {
                        return new EvaluationError(negativeWeightError);
                    }
                    sum += value * weight;
                    count += weight;
                }
            }
        }
        if (count === 0) {
            return new DivisionByZeroError(_t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
        }
        return { value: sum / count, format: inferFormat(args[0]) };
    },
};
// -----------------------------------------------------------------------------
// AVERAGEA
// -----------------------------------------------------------------------------
const AVERAGEA = {
    description: _t("Numerical average value in a dataset."),
    args: [
        arg("value1 (number, range<number>)", _t("The first value or range to consider when calculating the average value.")),
        arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to consider when calculating the average value.")),
    ],
    compute: function (...args) {
        let count = 0;
        const sum = reduceNumbersTextAs0(args, (acc, a) => {
            count += 1;
            return acc + a;
        }, 0, this.locale);
        if (count === 0) {
            return new DivisionByZeroError(_t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
        }
        return {
            value: sum / count,
            format: inferFormat(args[0]),
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// AVERAGEIF
// -----------------------------------------------------------------------------
const AVERAGEIF = {
    description: _t("Average of values depending on criteria."),
    args: [
        arg("criteria_range (number, range<number>)", _t("The range to check against criterion.")),
        arg("criterion (string)", _t("The pattern or test to apply to criteria_range.")),
        arg("average_range (number, range<number>, default=criteria_range)", _t("The range to average. If not included, criteria_range is used for the average instead.")),
    ],
    compute: function (criteriaRange, criterion, averageRange) {
        const _averageRange = averageRange === undefined ? toMatrix(criteriaRange) : toMatrix(averageRange);
        let count = 0;
        let sum = 0;
        visitMatchingRanges([criteriaRange, criterion], (i, j) => {
            const value = _averageRange[i]?.[j]?.value;
            if (typeof value === "number") {
                count += 1;
                sum += value;
            }
        }, this.locale);
        if (count === 0) {
            return new DivisionByZeroError(_t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
        }
        return sum / count;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// AVERAGEIFS
// -----------------------------------------------------------------------------
const AVERAGEIFS = {
    description: _t("Average of values depending on multiple criteria."),
    args: [
        arg("average_range (range)", _t("The range to average.")),
        arg("criteria_range1 (range)", _t("The range to check against criterion1.")),
        arg("criterion1 (string)", _t("The pattern or test to apply to criteria_range1.")),
        arg("criteria_range2 (any, range, repeating)", _t("Additional criteria_range and criterion to check.")),
        arg("criterion2 (string, repeating)", _t("The pattern or test to apply to criteria_range2.")),
    ],
    compute: function (averageRange, ...args) {
        const _averageRange = toMatrix(averageRange);
        let count = 0;
        let sum = 0;
        visitMatchingRanges(args, (i, j) => {
            const value = _averageRange[i]?.[j]?.value;
            if (typeof value === "number") {
                count += 1;
                sum += value;
            }
        }, this.locale);
        if (count === 0) {
            return new DivisionByZeroError(_t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
        }
        return sum / count;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// COUNT
// -----------------------------------------------------------------------------
const COUNT = {
    description: _t("The number of numeric values in dataset."),
    args: [
        arg("value1 (number, any, range<number>)", _t("The first value or range to consider when counting.")),
        arg("value2 (number, any, range<number>, repeating)", _t("Additional values or ranges to consider when counting.")),
    ],
    compute: function (...values) {
        return countNumbers(values, this.locale);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// COUNTA
// -----------------------------------------------------------------------------
const COUNTA = {
    description: _t("The number of values in a dataset."),
    args: [
        arg("value1 (any, range)", _t("The first value or range to consider when counting.")),
        arg("value2 (any, range, repeating)", _t("Additional values or ranges to consider when counting.")),
    ],
    compute: function (...values) {
        return countAny(values);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// COVAR
// -----------------------------------------------------------------------------
// Note: Unlike the VAR function which corresponds to the variance over a sample (VAR.S),
// the COVAR function corresponds to the covariance over an entire population (COVAR.P)
const COVAR = {
    description: _t("The covariance of a dataset."),
    args: [
        arg("data_y (any, range)", _t("The range representing the array or matrix of dependent data.")),
        arg("data_x (any, range)", _t("The range representing the array or matrix of independent data.")),
    ],
    compute: function (dataY, dataX) {
        return covariance(dataY, dataX, false);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// COVARIANCE.P
// -----------------------------------------------------------------------------
const COVARIANCE_P = {
    description: _t("The covariance of a dataset."),
    args: [
        arg("data_y (any, range)", _t("The range representing the array or matrix of dependent data.")),
        arg("data_x (any, range)", _t("The range representing the array or matrix of independent data.")),
    ],
    compute: function (dataY, dataX) {
        return covariance(dataY, dataX, false);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// COVARIANCE.S
// -----------------------------------------------------------------------------
const COVARIANCE_S = {
    description: _t("The sample covariance of a dataset."),
    args: [
        arg("data_y (any, range)", _t("The range representing the array or matrix of dependent data.")),
        arg("data_x (any, range)", _t("The range representing the array or matrix of independent data.")),
    ],
    compute: function (dataY, dataX) {
        return covariance(dataY, dataX, true);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// FORECAST
// -----------------------------------------------------------------------------
const FORECAST = {
    description: _t("Calculates the expected y-value for a specified x based on a linear regression of a dataset."),
    args: [
        arg("x (number, range<number>)", _t("The value(s) on the x-axis to forecast.")),
        arg("data_y (range<number>)", _t("The range representing the array or matrix of dependent data.")),
        arg("data_x (range<number>)", _t("The range representing the array or matrix of independent data.")),
    ],
    compute: function (x, dataY, dataX) {
        const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX);
        if (flatDataX.length === 0 || flatDataY.length === 0) {
            return new NotAvailableError(noValidInputErrorMessage);
        }
        return predictLinearValues([flatDataY], [flatDataX], matrixMap(toMatrix(x), (value) => toNumber(value, this.locale)), true);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// GROWTH
// -----------------------------------------------------------------------------
const GROWTH = {
    description: _t("Fits points to exponential growth trend."),
    args: [
        arg("known_data_y (range<number>)", _t("The array or range containing dependent (y) values that are already known, used to curve fit an ideal exponential growth curve.")),
        arg("known_data_x (range<number>, default={1;2;3;...})", _t("The values of the independent variable(s) corresponding with known_data_y.")),
        arg("new_data_x (any, range, default=known_data_x)", _t("The data points to return the y values for on the ideal curve fit.")),
        arg("b (boolean, default=TRUE)", _t("Given a general exponential form of y = b*m^x for a curve fit, calculates b if TRUE or forces b to be 1 and only calculates the m values if FALSE."), CALCULATE_B_OPTIONS),
    ],
    compute: function (knownDataY, knownDataX = [[]], newDataX = [[]], b = { value: true }) {
        if (knownDataY.length === 0 || knownDataY[0].length === 0) {
            return new EvaluationError(emptyDataErrorMessage("known_data_y"));
        }
        return expM(predictLinearValues(logM(toNumberMatrix(knownDataY, "known_data_y")), toNumberMatrix(knownDataX, "known_data_x"), toNumberMatrix(newDataX, "new_data_y"), toBoolean(b)));
    },
};
// -----------------------------------------------------------------------------
// INTERCEPT
// -----------------------------------------------------------------------------
const INTERCEPT = {
    description: _t("Compute the intercept of the linear regression."),
    args: [
        arg("data_y (range<number>)", _t("The range representing the array or matrix of dependent data.")),
        arg("data_x (range<number>)", _t("The range representing the array or matrix of independent data.")),
    ],
    compute: function (dataY, dataX) {
        const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX);
        if (flatDataX.length === 0 || flatDataY.length === 0) {
            return new NotAvailableError(noValidInputErrorMessage);
        }
        const [[], [intercept]] = fullLinearRegression([flatDataX], [flatDataY]);
        return intercept;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// LARGE
// -----------------------------------------------------------------------------
const LARGE = {
    description: _t("Nth largest element from a data set."),
    args: [
        arg("data (any, range)", _t("Array or range containing the dataset to consider.")),
        arg("n (number)", _t("The rank from largest to smallest of the element to return.")),
    ],
    compute: function (data, n) {
        const _n = Math.trunc(toNumber(n?.value, this.locale));
        const largests = [];
        let index;
        let count = 0;
        visitAny([data], (d) => {
            if (typeof d?.value === "number") {
                index = dichotomicSearch(largests, d, "nextSmaller", "asc", largests.length, (array, i) => array[i].value);
                largests.splice(index + 1, 0, d);
                count++;
                if (count > _n) {
                    largests.shift();
                    count--;
                }
            }
        });
        const result = largests.shift();
        if (result === undefined) {
            return new EvaluationError(noValidInputErrorMessage);
        }
        if (count < _n) {
            return new EvaluationError(_t("Function [[FUNCTION_NAME]] parameter 2 value (%s) is out of range.", _n));
        }
        return result;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// LINEST
// -----------------------------------------------------------------------------
const LINEST = {
    description: _t("Given partial data about a linear trend, calculates various parameters about the ideal linear trend using the least-squares method."),
    args: [
        arg("data_y (range<number>)", _t("The range representing the array or matrix of dependent data.")),
        arg("data_x (range<number>, default={1;2;3;...})", _t("The range representing the array or matrix of independent data.")),
        arg("calculate_b (boolean, default=TRUE)", _t("A flag specifying whether to compute the slope or not"), CALCULATE_B_OPTIONS),
        arg("verbose (boolean, default=FALSE)", _t("A flag specifying whether to return additional regression statistics or only the linear coefficients and the y-intercept"), RETURN_VERBOSE_OPTIONS),
    ],
    compute: function (dataY, dataX = [[]], calculateB = { value: true }, verbose = { value: false }) {
        if (dataY.length === 0 || dataY[0].length === 0) {
            return new EvaluationError(emptyDataErrorMessage("data_y"));
        }
        return fullLinearRegression(toNumberMatrix(dataX, "data_x"), toNumberMatrix(dataY, "data_y"), toBoolean(calculateB), toBoolean(verbose));
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// LOGEST
// -----------------------------------------------------------------------------
const LOGEST = {
    description: _t("Given partial data about an exponential growth curve, calculates various parameters about the best fit ideal exponential growth curve."),
    args: [
        arg("data_y (range<number>)", _t("The range representing the array or matrix of dependent data.")),
        arg("data_x (range<number>, optional, default={1;2;3;...})", _t("The range representing the array or matrix of independent data.")),
        arg("calculate_b (boolean, default=TRUE)", _t("A flag specifying whether to compute the slope or not"), CALCULATE_B_OPTIONS),
        arg("verbose (boolean, default=FALSE)", _t("A flag specifying whether to return additional regression statistics or only the linear coefficients and the y-intercept"), RETURN_VERBOSE_OPTIONS),
    ],
    compute: function (dataY, dataX = [[]], calculateB = { value: true }, verbose = { value: false }) {
        if (dataY.length === 0 || dataY[0].length === 0) {
            return new EvaluationError(emptyDataErrorMessage("data_y"));
        }
        const coeffs = fullLinearRegression(toNumberMatrix(dataX, "data_x"), logM(toNumberMatrix(dataY, "data_y")), toBoolean(calculateB), toBoolean(verbose));
        for (let i = 0; i < coeffs.length; i++) {
            coeffs[i][0] = Math.exp(coeffs[i][0]);
        }
        return coeffs;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// MATTHEWS
// -----------------------------------------------------------------------------
const MATTHEWS = {
    description: _t("Compute the Matthews correlation coefficient of a dataset."),
    args: [
        arg("data_x (range)", _t("The range representing the array or matrix of observed data.")),
        arg("data_y (range)", _t("The range representing the array or matrix of predicted data.")),
    ],
    compute: function (dataX, dataY) {
        const flatX = dataX.flat();
        const flatY = dataY.flat();
        assertSameNumberOfElements(flatX, flatY);
        if (flatX.length === 0 || flatY.length === 0) {
            return new NotAvailableError(noValidInputErrorMessage);
        }
        const n = flatX.length;
        let trueN = 0, trueP = 0, falseP = 0, falseN = 0;
        for (let i = 0; i < n; ++i) {
            const isTrue1 = toBoolean(flatX[i]);
            const isTrue2 = toBoolean(flatY[i]);
            if (isTrue1 === isTrue2) {
                if (isTrue1) {
                    trueP++;
                }
                else {
                    trueN++;
                }
            }
            else {
                if (isTrue1) {
                    falseN++;
                }
                else {
                    falseP++;
                }
            }
        }
        return ((trueP * trueN - falseP * falseN) /
            Math.sqrt((trueP + falseP) * (trueP + falseN) * (trueN + falseP) * (trueN + falseN)));
    },
    isExported: false,
};
// -----------------------------------------------------------------------------
// MAX
// -----------------------------------------------------------------------------
const MAX = {
    description: _t("Maximum value in a numeric dataset."),
    args: [
        arg("value1 (number, range<number>)", _t("The first value or range to consider when calculating the maximum value.")),
        arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to consider when calculating the maximum value.")),
    ],
    compute: function (...values) {
        return max(values, this.locale);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// MAXA
// -----------------------------------------------------------------------------
const MAXA = {
    description: _t("Maximum numeric value in a dataset."),
    args: [
        arg("value1 (any, range)", _t("The first value or range to consider when calculating the maximum value.")),
        arg("value2 (any, range, repeating)", _t("Additional values or ranges to consider when calculating the maximum value.")),
    ],
    compute: function (...args) {
        const maxa = reduceNumbersTextAs0(args, (acc, a) => {
            return Math.max(a, acc);
        }, -Infinity, this.locale);
        return { value: maxa === -Infinity ? 0 : maxa, format: inferFormat(args[0]) };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// MAXIFS
// -----------------------------------------------------------------------------
const MAXIFS = {
    description: _t("Returns the maximum value in a range of cells, filtered by a set of criteria."),
    args: [
        arg("range (range)", _t("The range of cells from which the maximum will be determined.")),
        arg("criteria_range1 (range)", _t("The range of cells over which to evaluate criterion1.")),
        arg("criterion1 (string)", _t("The pattern or test to apply to criteria_range1, such that each cell that evaluates to TRUE will be included in the filtered set.")),
        arg("criteria_range2 (any, range, repeating)", _t("Additional ranges over which to evaluate the additional criteria. The filtered set will be the intersection of the sets produced by each criterion-range pair.")),
        arg("criterion2 (string, repeating)", _t("The pattern or test to apply to criteria_range2.")),
    ],
    compute: function (range, ...args) {
        let result = -Infinity;
        visitMatchingRanges(args, (i, j) => {
            const value = range[i]?.[j]?.value;
            if (typeof value === "number") {
                result = result < value ? value : result;
            }
        }, this.locale);
        return result === -Infinity ? 0 : result;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// MEDIAN
// -----------------------------------------------------------------------------
const MEDIAN = {
    description: _t("Median value in a numeric dataset."),
    args: [
        arg("value1 (any, range)", _t("The first value or range to consider when calculating the median value.")),
        arg("value2 (any, range, repeating)", _t("Additional values or ranges to consider when calculating the median value.")),
    ],
    compute: function (...values) {
        const data = [];
        visitNumbers(values, (value) => {
            data.push(value);
        }, this.locale);
        return {
            value: centile(data, { value: 0.5 }, true, this.locale),
            format: inferFormat(data[0]),
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// MIN
// -----------------------------------------------------------------------------
const MIN = {
    description: _t("Minimum value in a numeric dataset."),
    args: [
        arg("value1 (number, range<number>)", _t("The first value or range to consider when calculating the minimum value.")),
        arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to consider when calculating the minimum value.")),
    ],
    compute: function (...values) {
        return min(values, this.locale);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// MINA
// -----------------------------------------------------------------------------
const MINA = {
    description: _t("Minimum numeric value in a dataset."),
    args: [
        arg("value1 (number, range<number>)", _t("The first value or range to consider when calculating the minimum value.")),
        arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to consider when calculating the minimum value.")),
    ],
    compute: function (...args) {
        const mina = reduceNumbersTextAs0(args, (acc, a) => {
            return Math.min(a, acc);
        }, Infinity, this.locale);
        return { value: mina === Infinity ? 0 : mina, format: inferFormat(args[0]) };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// MINIFS
// -----------------------------------------------------------------------------
const MINIFS = {
    description: _t("Returns the minimum value in a range of cells, filtered by a set of criteria."),
    args: [
        arg("range (range)", _t("The range of cells from which the minimum will be determined.")),
        arg("criteria_range1 (range)", _t("The range of cells over which to evaluate criterion1.")),
        arg("criterion1 (string)", _t("The pattern or test to apply to criteria_range1, such that each cell that evaluates to TRUE will be included in the filtered set.")),
        arg("criteria_range2 (any, range, repeating)", _t("Additional ranges over which to evaluate the additional criteria. The filtered set will be the intersection of the sets produced by each criterion-range pair.")),
        arg("criterion2 (string, repeating)", _t("The pattern or test to apply to criteria_range2.")),
    ],
    compute: function (range, ...args) {
        let result = Infinity;
        visitMatchingRanges(args, (i, j) => {
            const value = range[i]?.[j]?.value;
            if (typeof value === "number") {
                result = result > value ? value : result;
            }
        }, this.locale);
        return result === Infinity ? 0 : result;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// PEARSON
// -----------------------------------------------------------------------------
function pearson(dataY, dataX) {
    const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX);
    if (flatDataX.length === 0 || flatDataY.length === 0) {
        return new NotAvailableError(noValidInputErrorMessage);
    }
    const n = flatDataX.length;
    let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0, sumYY = 0;
    for (let i = 0; i < n; i++) {
        const xij = flatDataX[i];
        const yij = flatDataY[i];
        sumX += xij;
        sumY += yij;
        sumXY += xij * yij;
        sumXX += xij * xij;
        sumYY += yij * yij;
    }
    return ((n * sumXY - sumX * sumY) / Math.sqrt((n * sumXX - sumX * sumX) * (n * sumYY - sumY * sumY)));
}
const PEARSON = {
    description: _t("Compute the Pearson product-moment correlation coefficient of a dataset."),
    args: [
        arg("data_y (range<number>)", _t("The range representing the array or matrix of dependent data.")),
        arg("data_x (range<number>)", _t("The range representing the array or matrix of independent data.")),
    ],
    compute: function (dataY, dataX) {
        return pearson(dataY, dataX);
    },
    isExported: true,
};
// CORREL
// In GSheet, CORREL is just an alias to PEARSON
const CORREL = PEARSON;
// -----------------------------------------------------------------------------
// PERCENTILE
// -----------------------------------------------------------------------------
const PERCENTILE = {
    description: _t("Value at a given percentile of a dataset."),
    args: [
        arg("data (any, range)", _t("The array or range containing the dataset to consider.")),
        arg("percentile (number)", _t("The percentile whose value within data will be calculated and returned.")),
    ],
    compute: function (data, percentile) {
        return PERCENTILE_INC.compute.bind(this)(data, percentile);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// PERCENTILE.EXC
// -----------------------------------------------------------------------------
const PERCENTILE_EXC = {
    description: _t("Value at a given percentile of a dataset exclusive of 0 and 1."),
    args: [
        arg("data (any, range)", _t("The array or range containing the dataset to consider.")),
        arg("percentile (number)", _t("The percentile, exclusive of 0 and 1, whose value within 'data' will be calculated and returned.")),
    ],
    compute: function (data, percentile) {
        return {
            value: centile([data], percentile, false, this.locale),
            format: inferFormat(data),
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// PERCENTILE.INC
// -----------------------------------------------------------------------------
const PERCENTILE_INC = {
    description: _t("Value at a given percentile of a dataset."),
    args: [
        arg("data (any, range)", _t("The array or range containing the dataset to consider.")),
        arg("percentile (number)", _t("The percentile whose value within data will be calculated and returned.")),
    ],
    compute: function (data, percentile) {
        return {
            value: centile([data], percentile, true, this.locale),
            format: inferFormat(data),
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// POLYFIT
// -----------------------------------------------------------------------------
const POLYFIT_COEFFS = {
    description: _t("Compute the coefficients of polynomial regression of the dataset."),
    args: [
        arg("data_y (range<number>)", _t("The range representing the array or matrix of dependent data.")),
        arg("data_x (range<number>)", _t("The range representing the array or matrix of independent data.")),
        arg("order (number)", _t("The order of the polynomial to fit the data, between 1 and 6."), POLYNOMIAL_ORDER_OPTIONS),
        arg("intercept (boolean, default=TRUE)", _t("A flag specifying whether to compute the intercept or not."), COMPUTE_INTERCEPT_OPTIONS),
    ],
    compute: function (dataY, dataX, order, intercept = { value: true }) {
        const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX);
        if (flatDataX.length === 0 || flatDataY.length === 0) {
            return new NotAvailableError(noValidInputErrorMessage);
        }
        return polynomialRegression(flatDataY, flatDataX, toNumber(order, this.locale), toBoolean(intercept));
    },
    isExported: false,
};
// -----------------------------------------------------------------------------
// POLYFIT.FORECAST
// -----------------------------------------------------------------------------
const POLYFIT_FORECAST = {
    description: _t("Predict value by computing a polynomial regression of the dataset."),
    args: [
        arg("x (number, range<number>)", _t("The value(s) on the x-axis to forecast.")),
        arg("data_y (range<number>)", _t("The range representing the array or matrix of dependent data.")),
        arg("data_x (range<number>)", _t("The range representing the array or matrix of independent data.")),
        arg("order (number)", _t("The order of the polynomial to fit the data, between 1 and 6."), POLYNOMIAL_ORDER_OPTIONS),
        arg("intercept (boolean, default=TRUE)", _t("A flag specifying whether to compute the intercept or not."), COMPUTE_INTERCEPT_OPTIONS),
    ],
    compute: function (x, dataY, dataX, order, intercept = { value: true }) {
        const _order = toNumber(order, this.locale);
        const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX);
        if (flatDataX.length === 0 || flatDataY.length === 0) {
            return new NotAvailableError(noValidInputErrorMessage);
        }
        const coeffs = polynomialRegression(flatDataY, flatDataX, _order, toBoolean(intercept)).flat();
        return matrixMap(toMatrix(x), (xij) => evaluatePolynomial(coeffs, toNumber(xij, this.locale), _order));
    },
    isExported: false,
};
// -----------------------------------------------------------------------------
// QUARTILE
// -----------------------------------------------------------------------------
const QUARTILE = {
    description: _t("Value nearest to a specific quartile of a dataset."),
    args: [
        arg("data (any, range)", _t("The array or range containing the dataset to consider.")),
        arg("quartile_number (number)", _t("Which quartile value to return."), QUARTILE_NUMBER_OPTIONS),
    ],
    compute: function (data, quartileNumber) {
        return QUARTILE_INC.compute.bind(this)(data, quartileNumber);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// QUARTILE.EXC
// -----------------------------------------------------------------------------
const QUARTILE_EXC = {
    description: _t("Value nearest to a specific quartile of a dataset exclusive of 0 and 4."),
    args: [
        arg("data (any, range)", _t("The array or range containing the dataset to consider.")),
        arg("quartile_number (number)", _t("Which quartile value, exclusive of 0 and 4, to return."), [
            { value: 1, label: _t("First quartile (25th percentile)") },
            { value: 2, label: _t("Median value (50th percentile)") },
            { value: 3, label: _t("Third quartile (75th percentile)") },
        ]),
    ],
    compute: function (data, quartileNumber) {
        const _quartileNumber = Math.trunc(toNumber(quartileNumber, this.locale));
        const percent = { value: 0.25 * _quartileNumber };
        return {
            value: centile([data], percent, false, this.locale),
            format: inferFormat(data),
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// QUARTILE.INC
// -----------------------------------------------------------------------------
const QUARTILE_INC = {
    description: _t("Value nearest to a specific quartile of a dataset."),
    args: [
        arg("data (any, range)", _t("The array or range containing the dataset to consider.")),
        arg("quartile_number (number)", _t("Which quartile value to return."), QUARTILE_NUMBER_OPTIONS),
    ],
    compute: function (data, quartileNumber) {
        const percent = { value: 0.25 * Math.trunc(toNumber(quartileNumber, this.locale)) };
        return {
            value: centile([data], percent, true, this.locale),
            format: inferFormat(data),
        };
    },
    isExported: true,
};
// RANK
// -----------------------------------------------------------------------------
const RANK = {
    description: _t("Returns the rank of a specified value in a dataset."),
    args: [
        arg("value (number)", _t("The value whose rank will be determined.")),
        arg("data (range)", _t("The range containing the dataset to consider.")),
        arg("is_ascending (boolean, default=FALSE)", _t("Whether to consider the values in data in descending or ascending order."), [
            { value: false, label: _t("Descending") },
            { value: true, label: _t("Ascending") },
        ]),
    ],
    compute: function (value, data, isAscending = { value: false }) {
        const _isAscending = toBoolean(isAscending);
        const _value = toNumber(value, this.locale);
        let rank = 1;
        let found = false;
        for (const row of data) {
            for (const cell of row) {
                if (typeof cell.value !== "number") {
                    continue;
                }
                const _cell = toNumber(cell, this.locale);
                if (_cell === _value) {
                    found = true;
                }
                else if (_cell > _value !== _isAscending) {
                    rank++;
                }
            }
        }
        if (!found) {
            return new NotAvailableError(_t("Value not found in the given data."));
        }
        return rank;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// RSQ
// -----------------------------------------------------------------------------
const RSQ = {
    description: _t("Compute the square of r, the Pearson product-moment correlation coefficient of a dataset."),
    args: [
        arg("data_y (range<number>)", _t("The range representing the array or matrix of dependent data.")),
        arg("data_x (range<number>)", _t("The range representing the array or matrix of independent data.")),
    ],
    compute: function (dataY, dataX) {
        const value = pearson(dataY, dataX);
        if (value instanceof Error) {
            throw value;
        }
        return Math.pow(value, 2.0);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// SLOPE
// -----------------------------------------------------------------------------
const SLOPE = {
    description: _t("Compute the slope of the linear regression."),
    args: [
        arg("data_y (range<number>)", _t("The range representing the array or matrix of dependent data.")),
        arg("data_x (range<number>)", _t("The range representing the array or matrix of independent data.")),
    ],
    compute: function (dataY, dataX) {
        const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX);
        if (flatDataX.length === 0 || flatDataY.length === 0) {
            return new NotAvailableError(noValidInputErrorMessage);
        }
        const [[slope]] = fullLinearRegression([flatDataX], [flatDataY]);
        return slope;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// SMALL
// -----------------------------------------------------------------------------
const SMALL = {
    description: _t("Nth smallest element in a data set."),
    args: [
        arg("data (any, range)", _t("The array or range containing the dataset to consider.")),
        arg("n (number)", _t("The rank from smallest to largest of the element to return.")),
    ],
    compute: function (data, n) {
        const _n = Math.trunc(toNumber(n?.value, this.locale));
        const largests = [];
        let index;
        let count = 0;
        visitAny([data], (d) => {
            if (typeof d?.value === "number") {
                index = dichotomicSearch(largests, d, "nextSmaller", "asc", largests.length, (array, i) => array[i].value);
                largests.splice(index + 1, 0, d);
                count++;
                if (count > _n) {
                    largests.pop();
                    count--;
                }
            }
        });
        const result = largests.pop();
        if (result === undefined) {
            return new EvaluationError(noValidInputErrorMessage);
        }
        if (count < _n) {
            return new EvaluationError(_t("Function [[FUNCTION_NAME]] parameter 2 value (%s) is out of range.", _n));
        }
        return result;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// SPEARMAN
// -----------------------------------------------------------------------------
const SPEARMAN = {
    description: _t("Compute the Spearman rank correlation coefficient of a dataset."),
    args: [
        arg("data_y (range<number>)", _t("The range representing the array or matrix of dependent data.")),
        arg("data_x (range<number>)", _t("The range representing the array or matrix of independent data.")),
    ],
    compute: function (dataX, dataY) {
        const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX);
        if (flatDataX.length === 0 || flatDataY.length === 0) {
            return new NotAvailableError(noValidInputErrorMessage);
        }
        const n = flatDataX.length;
        const order = flatDataX.map((e, i) => [e, flatDataY[i]]);
        order.sort((a, b) => a[0] - b[0]);
        for (let i = 0; i < n; ++i) {
            order[i][0] = i;
        }
        order.sort((a, b) => a[1] - b[1]);
        let sum = 0.0;
        for (let i = 0; i < n; ++i) {
            sum += (order[i][0] - i) ** 2;
        }
        return 1 - (6 * sum) / (n ** 3 - n);
    },
    isExported: false,
};
// -----------------------------------------------------------------------------
// STDEV
// -----------------------------------------------------------------------------
const STDEV = {
    description: _t("Standard deviation."),
    args: [
        arg("value1 (number, range<number>)", _t("The first value or range of the sample.")),
        arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to include in the sample.")),
    ],
    compute: function (...args) {
        return Math.sqrt(VAR.compute.bind(this)(...args));
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// STDEV.P
// -----------------------------------------------------------------------------
const STDEV_P = {
    description: _t("Standard deviation of entire population."),
    args: [
        arg("value1 (number, range<number>)", _t("The first value or range of the population.")),
        arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to include in the population.")),
    ],
    compute: function (...args) {
        return Math.sqrt(VAR_P.compute.bind(this)(...args));
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// STDEV.S
// -----------------------------------------------------------------------------
const STDEV_S = {
    description: _t("Standard deviation."),
    args: [
        arg("value1 (number, range<number>)", _t("The first value or range of the sample.")),
        arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to include in the sample.")),
    ],
    compute: function (...args) {
        return Math.sqrt(VAR_S.compute.bind(this)(...args));
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// STDEVA
// -----------------------------------------------------------------------------
const STDEVA = {
    description: _t("Standard deviation of sample (text as 0)."),
    args: [
        arg("value1 (number, range<number>)", _t("The first value or range of the sample.")),
        arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to include in the sample.")),
    ],
    compute: function (...args) {
        return Math.sqrt(VARA.compute.bind(this)(...args));
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// STDEVP
// -----------------------------------------------------------------------------
const STDEVP = {
    description: _t("Standard deviation of entire population."),
    args: [
        arg("value1 (number, range<number>)", _t("The first value or range of the population.")),
        arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to include in the population.")),
    ],
    compute: function (...args) {
        return Math.sqrt(VARP.compute.bind(this)(...args));
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// STDEVPA
// -----------------------------------------------------------------------------
const STDEVPA = {
    description: _t("Standard deviation of entire population (text as 0)."),
    args: [
        arg("value1 (number, range<number>)", _t("The first value or range of the population.")),
        arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to include in the population.")),
    ],
    compute: function (...args) {
        return Math.sqrt(VARPA.compute.bind(this)(...args));
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// STEYX
// -----------------------------------------------------------------------------
const STEYX = {
    description: _t("Calculates the standard error of the predicted y-value for each x in the regression of a dataset."),
    args: [
        arg("data_y (range<number>)", _t("The range representing the array or matrix of dependent data.")),
        arg("data_x (range<number>)", _t("The range representing the array or matrix of independent data.")),
    ],
    compute: function (dataY, dataX) {
        const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX);
        if (flatDataX.length === 0 || flatDataY.length === 0) {
            return new NotAvailableError(noValidInputErrorMessage);
        }
        const data = fullLinearRegression([flatDataX], [flatDataY], true, true);
        return data[1][2];
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// TREND
// -----------------------------------------------------------------------------
const TREND = {
    description: _t("Fits points to linear trend derived via least-squares."),
    args: [
        arg("known_data_y (number, range<number>)", _t("The array or range containing dependent (y) values that are already known, used to curve fit an ideal linear trend.")),
        arg("known_data_x (number, range<number>, optional, default={1;2;3;...})", _t("The values of the independent variable(s) corresponding with known_data_y.")),
        arg("new_data_x (number, range<number>, optional, default=known_data_x)", _t("The data points to return the y values for on the ideal curve fit.")),
        arg("b (boolean, optional, default=TRUE)", _t("Given a general linear form of y = m*x+b for a curve fit, calculates b if TRUE or forces b to be 0 and only calculates the m values if FALSE, i.e. forces the curve fit to pass through the origin."), CALCULATE_B_OPTIONS),
    ],
    compute: function (knownDataY, knownDataX = [[]], newDataX = [[]], b = { value: true }) {
        if (knownDataY.length === 0 || knownDataY[0].length === 0) {
            return new EvaluationError(emptyDataErrorMessage("known_data_y"));
        }
        return predictLinearValues(toNumberMatrix(knownDataY, "known_data_y"), toNumberMatrix(knownDataX, "known_data_x"), toNumberMatrix(newDataX, "new_data_y"), toBoolean(b));
    },
};
// -----------------------------------------------------------------------------
// VAR
// -----------------------------------------------------------------------------
const VAR = {
    description: _t("Variance."),
    args: [
        arg("value1 (number, range<number>)", _t("The first value or range of the sample.")),
        arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to include in the sample.")),
    ],
    compute: function (...args) {
        return variance(args, true, false, this.locale);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// VAR.P
// -----------------------------------------------------------------------------
const VAR_P = {
    description: _t("Variance of entire population."),
    args: [
        arg("value1 (number, range<number>)", _t("The first value or range of the population.")),
        arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to include in the population.")),
    ],
    compute: function (...args) {
        return variance(args, false, false, this.locale);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// VAR.S
// -----------------------------------------------------------------------------
const VAR_S = {
    description: _t("Variance."),
    args: [
        arg("value1 (number, range<number>)", _t("The first value or range of the sample.")),
        arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to include in the sample.")),
    ],
    compute: function (...args) {
        return variance(args, true, false, this.locale);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// VARA
// -----------------------------------------------------------------------------
const VARA = {
    description: _t("Variance of sample (text as 0)."),
    args: [
        arg("value1 (number, range<number>)", _t("The first value or range of the sample.")),
        arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to include in the sample.")),
    ],
    compute: function (...args) {
        return variance(args, true, true, this.locale);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// VARP
// -----------------------------------------------------------------------------
const VARP = {
    description: _t("Variance of entire population."),
    args: [
        arg("value1 (number, range<number>)", _t("The first value or range of the population.")),
        arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to include in the population.")),
    ],
    compute: function (...args) {
        return variance(args, false, false, this.locale);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// VARPA
// -----------------------------------------------------------------------------
const VARPA = {
    description: _t("Variance of entire population (text as 0)."),
    args: [
        arg("value1 (number, range<number>)", _t("The first value or range of the population.")),
        arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to include in the population.")),
    ],
    compute: function (...args) {
        return variance(args, false, true, this.locale);
    },
    isExported: true,
};

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

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

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

const expectCashFlowsAndDatesHaveSameDimension = _t("The cashflow_amounts and cashflow_dates ranges must have the same dimensions.");
const expectCashFlowsHavePositiveAndNegativesValues = _t("There must be both positive and negative values in cashflow_amounts.");
const expectCostPositiveOrZero = (cost) => _t("The cost (%s) must be positive or null.", cost);
const expectCostStrictlyPositive = (cost) => _t("The cost (%s) must be strictly positive.", cost);
const expectCouponFrequencyIsValid = (frequency) => _t("The frequency (%s) must be one of %s", frequency, [1, 2, 4].toString());
const expectDayCountConventionIsValid = (dayCountConvention) => _t("The day_count_convention (%s) must be between 0 and 4 inclusive.", dayCountConvention);
const expectDeprecationFactorStrictlyPositive = (factor) => _t("The depreciation factor (%s) must be strictly positive.", factor);
const expectDiscountDifferentFromMinusOne = (discount) => _t("The discount (%s) must be different from -1.", discount);
const expectDiscountStrictlyPositive = (discount) => _t("The discount (%s) must be strictly positive.", discount);
const expectDiscountStrictlySmallerThanOne = (discount) => _t("The discount (%s) must be smaller than 1.", discount);
const expectEffectiveRateStrictlyPositive = (effectiveRate) => _t("The effective_rate (%s) must be strictly positive.", effectiveRate);
const expectEndPeriodPositiveOrZero = (endPeriod) => _t("The end_period (%s) must be positive or null.", endPeriod);
const expectEndPeriodSmallerOrEqualToLife = (end, life) => _t("The end_period (%(end)s) must be smaller or equal to the life (%(life)s).", { end, life });
const expectEveryDateGreaterThanFirstDateOfCashFlowDates = (firstDate) => _t("All the dates should be greater or equal to the first date in cashflow_dates (%s).", firstDate);
const expectFirstPeriodSmallerOrEqualLastPeriod = (first, last) => _t("The first_period (%(first)s) must be smaller or equal to the last_period (%(last)s).", {
    first,
    last,
});
const expectFirstPeriodStrictlyPositive = (period) => _t("The first_period (%s) must be strictly positive.", period);
const expectFutureValueStrictlyPositive = (pv) => _t("The future_value (%s) must be strictly positive.", pv);
const expectInvestmentStrictlyPositive = (investment) => _t("The investment (%s) must be strictly positive.", investment);
const expectIssuePositiveOrZero = (issue) => _t("The issue (%s) must be positive or null.", issue);
const expectLastPeriodSmallerOrEqualNumberOfPeriods = (last, nPeriods) => _t("The last_period (%(last)s) must be smaller or equal to the number_of_periods (%(nPeriods)s).", { last, nPeriods });
const expectLastPeriodStrictlyPositive = (period) => _t("The last_period (%s) must be strictly positive.", period);
const expectLifeStrictlyPositive = (life) => _t("The life (%s) must be strictly positive.", life);
const expectMaturityStrictlyGreaterThanSettlement = (settlement, maturity) => _t("The maturity (%(maturity)s) must be strictly greater than the settlement (%(settlement)s).", {
    maturity,
    settlement,
});
const expectMonthBetweenOneAndTwelve = (month) => _t("The month (%s) must be between 1 and 12 inclusive.", month);
const expectNominalRateStrictlyPositive = (nominalRate) => _t("The nominal_rate (%s) must be strictly positive.", nominalRate);
const expectNumberOfPeriodDifferentFromZero = (nPeriods) => _t("The number_of_periods (%s) must be different from zero.", nPeriods);
const expectNumberOfPeriodsStrictlyPositive = (nPeriods) => _t("The number_of_periods (%s) must be strictly positive.", nPeriods);
const expectPeriodBetweenOneAndNumberOfPeriods = (nPeriods) => _t("The period must be between 1 and number_of_periods (%s)", nPeriods);
const expectPeriodLessOrEqualToLifeLimit = (period, lifeLimit) => _t("The period (%(period)s) must be less than or equal to %(lifeLimit)s.", { period, lifeLimit });
const expectPeriodPositiveOrZero = (period) => _t("The period (%s) must be positive or null.", period);
const expectPeriodsByYearStrictlyPositive = (periodsByYear) => _t("The periods_by_year (%s) must be strictly positive.", periodsByYear);
const expectPeriodSmallerOrEqualToLife = (period, life) => _t("The period (%(period)s) must be less than or equal life (%(life)s).", { period, life });
const expectPeriodStrictlyPositive = (period) => _t("The period (%s) must be strictly positive.", period);
const expectPresentValueStrictlyPositive = (pv) => _t("The present_value (%s) must be strictly positive.", pv);
const expectPriceStrictlyPositive = (price) => _t("The price (%s) must be strictly positive.", price);
const expectPurchaseDateBeforeFirstPeriodEnd = (purchaseDate, firstPeriodEnd) => _t("The purchase_date (%(purchaseDate)s) must be before the first_period_end (%(firstPeriodEnd)s).", { purchaseDate, firstPeriodEnd });
const expectPurchaseDatePositiveOrZero = (purchaseDate) => _t("The purchase_date (%s) must be positive or null.", purchaseDate);
const expectRateGuessStrictlyGreaterThanMinusOne = (guess) => _t("The rate_guess (%s) must be strictly greater than -1.", guess);
const expectRatePositiveOrZero = (rate) => _t("The rate (%s) must be positive or null.", rate);
const expectRateStrictlyPositive = (rate) => _t("The rate (%s) must be strictly positive.", rate);
const expectRedemptionStrictlyPositive = (redemption) => _t("The redemption (%s) must be strictly positive.", redemption);
const expectSalvagePositiveOrZero = (salvage) => _t("The salvage (%s) must be positive or null.", salvage);
const expectSalvageSmallerOrEqualThanCost = (salvage, cost) => _t("The salvage (%(salvage)s) must be smaller or equal than the cost (%(cost)s).", {
    salvage,
    cost,
});
const expectSettlementGreaterOrEqualToIssue = (settlement, issue) => _t("The settlement date (%(settlement)s) must be greater or equal to the issue date (%(issue)s).", { settlement, issue });
const expectSettlementLessThanOneYearBeforeMaturity = (settlement, maturity) => _t("The settlement date (%(settlement)s) must at most one year after the maturity date (%(maturity)s).", { settlement, maturity });
const expectSettlementStrictlyGreaterThanIssue = (settlement, issue) => _t("The settlement date (%(settlement)s) must be strictly greater than the issue date (%(issue)s).", { settlement, issue });
const expectStartPeriodPositiveOrZero = (startPeriod) => _t("The start_period (%s) must be positive or null.", startPeriod);
const expectStartPeriodSmallerOrEqualEndPeriod = (start, end) => _t("The start_period (%(start)s) must be smaller or equal to the end_period (%(end)s).", {
    start,
    end,
});
const expectUnitStrictlyPositive = (unit) => _t("The unit (%s) must be strictly positive.", unit);
const expectYieldPositiveOrZero = (yeld) => _t("The yield (%s) must be positive or null.", yeld);
function havePositiveAndNegativeValues(arrayNumbers) {
    return arrayNumbers.some((val) => val > 0) && arrayNumbers.some((val) => val < 0);
}
function isInvalidDayCountConvention(dayCountConvention) {
    return ![0, 1, 2, 3, 4].includes(dayCountConvention);
}
function isInvalidFrequency(frequency) {
    return ![1, 2, 4].includes(frequency);
}
function isSettlementLessThanOneYearBeforeMaturity(settlement, maturity, locale) {
    const startDate = toJsDate(settlement, locale);
    const endDate = toJsDate(maturity, locale);
    const startDatePlusOneYear = toJsDate(settlement, locale);
    startDatePlusOneYear.setFullYear(startDate.getFullYear() + 1);
    return endDate.getTime() <= startDatePlusOneYear.getTime();
}
const DAY_COUNT_CONVENTION_OPTIONS = [
    { value: 0, label: _t("US (NASD) 30/360") },
    { value: 1, label: _t("Actual/Actual") },
    { value: 2, label: _t("Actual/360") },
    { value: 3, label: _t("Actual/365") },
    { value: 4, label: _t("European 30/360") },
];

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

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

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

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

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

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

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

const DEFAULT_DAY_COUNT_CONVENTION = 0;
const DEFAULT_END_OR_BEGINNING = 0;
const DEFAULT_FUTURE_VALUE = 0;
const FREQUENCY_OPTIONS = [
    { value: 1, label: _t("Annual") },
    { value: 2, label: _t("Semi-annual") },
    { value: 4, label: _t("Quarterly") },
];
const PAYMENT_TIMING_OPTIONS = [
    { value: 0, label: _t("End of period (default)") },
    { value: 1, label: _t("Beginning of period") },
];
const COUPON_FUNCTION_ARGS = [
    arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")),
    arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")),
    arg("frequency (number)", _t("The number of interest or coupon payments per year (1, 2, or 4)."), FREQUENCY_OPTIONS),
    arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use."), DAY_COUNT_CONVENTION_OPTIONS),
];
/**
 * Use the Newton–Raphson method to find a root of the given function in an iterative manner.
 *
 * @param func the function to find a root of
 * @param derivFunc the derivative of the function
 * @param startValue the initial value for the first iteration of the algorithm
 * @param maxIterations the maximum number of iterations
 * @param epsMax the epsilon for the root
 * @param nanFallback a function giving a fallback value to use if func(x) returns NaN. Useful if the
 *                       function is not defined for some range, but we know approximately where the root is when the Newton
 *                       algorithm ends up in this range.
 */
function newtonMethod(func, derivFunc, startValue, maxIterations, epsMax = 1e-10, nanFallback) {
    let x = startValue;
    let newX;
    let xDelta;
    let y;
    let yEqual0 = false;
    let count = 0;
    let previousFallback = undefined;
    do {
        y = func(x);
        if (isNaN(y)) {
            assert(count < maxIterations && nanFallback !== undefined, _t("Function [[FUNCTION_NAME]] didn't find any result."));
            count++;
            x = nanFallback(previousFallback);
            previousFallback = x;
            continue;
        }
        newX = x - y / derivFunc(x);
        xDelta = Math.abs(newX - x);
        x = newX;
        yEqual0 = xDelta < epsMax || Math.abs(y) < epsMax;
        assert(count < maxIterations, _t("Function [[FUNCTION_NAME]] didn't find any result."));
        count++;
    } while (!yEqual0);
    return x;
}
// -----------------------------------------------------------------------------
// ACCRINTM
// -----------------------------------------------------------------------------
const ACCRINTM = {
    description: _t("Accrued interest of security paying at maturity."),
    args: [
        arg("issue (date)", _t("The date the security was initially issued.")),
        arg("maturity (date)", _t("The maturity date of the security.")),
        arg("rate (number)", _t("The annualized rate of interest.")),
        arg("redemption (number)", _t("The redemption amount per 100 face value, or par.")),
        arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use."), DAY_COUNT_CONVENTION_OPTIONS),
    ],
    compute: function (issue, maturity, rate, redemption, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {
        const start = Math.trunc(toNumber(issue, this.locale));
        const end = Math.trunc(toNumber(maturity, this.locale));
        const _redemption = toNumber(redemption, this.locale);
        const _rate = toNumber(rate, this.locale);
        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
        if (start < 0) {
            return new EvaluationError(expectIssuePositiveOrZero(start));
        }
        if (start >= end) {
            return new EvaluationError(expectMaturityStrictlyGreaterThanSettlement(start, end));
        }
        if (isInvalidDayCountConvention(_dayCountConvention)) {
            return new EvaluationError(expectDayCountConventionIsValid(_dayCountConvention));
        }
        if (_redemption <= 0) {
            return new EvaluationError(expectRedemptionStrictlyPositive(_redemption));
        }
        if (_rate <= 0) {
            return new EvaluationError(expectRateStrictlyPositive(_rate));
        }
        const yearFrac = getYearFrac(start, end, _dayCountConvention);
        return _redemption * _rate * yearFrac;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// AMORLINC
// -----------------------------------------------------------------------------
const AMORLINC = {
    description: _t("Depreciation for an accounting period."),
    args: [
        arg("cost (number)", _t("The initial cost of the asset.")),
        arg("purchase_date (date)", _t("The date the asset was purchased.")),
        arg("first_period_end (date)", _t("The date the first period ended.")),
        arg("salvage (number)", _t("The value of the asset at the end of depreciation.")),
        arg("period (number)", _t("The single period within life for which to calculate depreciation.")),
        arg("rate (number)", _t("The deprecation rate.")),
        arg("day_count_convention (number, optional)", _t("An indicator of what day count method to use."), DAY_COUNT_CONVENTION_OPTIONS),
    ],
    compute: function (cost, purchaseDate, firstPeriodEnd, salvage, period, rate, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {
        dayCountConvention = dayCountConvention || 0;
        const _cost = toNumber(cost, this.locale);
        const _purchaseDate = Math.trunc(toNumber(purchaseDate, this.locale));
        const _firstPeriodEnd = Math.trunc(toNumber(firstPeriodEnd, this.locale));
        const _salvage = toNumber(salvage, this.locale);
        const _period = toNumber(period, this.locale);
        const _rate = toNumber(rate, this.locale);
        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
        if (_cost <= 0) {
            return new EvaluationError(expectCostStrictlyPositive(_cost));
        }
        if (_purchaseDate < 0) {
            return new EvaluationError(expectPurchaseDatePositiveOrZero(_purchaseDate));
        }
        if (_salvage < 0) {
            return new EvaluationError(expectSalvagePositiveOrZero(_salvage));
        }
        if (_salvage > _cost) {
            return new EvaluationError(expectSalvageSmallerOrEqualThanCost(_salvage, _cost));
        }
        if (_period < 0) {
            return new EvaluationError(expectPeriodPositiveOrZero(_period));
        }
        if (_rate <= 0) {
            return new EvaluationError(expectRateStrictlyPositive(_rate));
        }
        if (isInvalidDayCountConvention(_dayCountConvention)) {
            return new EvaluationError(expectDayCountConventionIsValid(_dayCountConvention));
        }
        if (_purchaseDate > _firstPeriodEnd) {
            return new EvaluationError(expectPurchaseDateBeforeFirstPeriodEnd(_purchaseDate, _firstPeriodEnd));
        }
        /**
         * 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 = getYearFrac(_purchaseDate, _firstPeriodEnd, _dayCountConvention);
        const firstDeprec = _purchaseDate === _firstPeriodEnd ? deprec : deprec * yearFrac;
        const valueAtPeriod = _cost - firstDeprec - deprec * roundedPeriod;
        if (valueAtPeriod >= _salvage) {
            return roundedPeriod === 0 ? firstDeprec : deprec;
        }
        return _salvage - valueAtPeriod < deprec ? deprec - (_salvage - valueAtPeriod) : 0;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// COUPDAYS
// -----------------------------------------------------------------------------
const COUPDAYS = {
    description: _t("Days in coupon period containing settlement date."),
    args: COUPON_FUNCTION_ARGS,
    compute: function (settlement, maturity, frequency, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {
        dayCountConvention = dayCountConvention || 0;
        const start = Math.trunc(toNumber(settlement, this.locale));
        const end = Math.trunc(toNumber(maturity, this.locale));
        const _frequency = Math.trunc(toNumber(frequency, this.locale));
        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
        if (start >= end) {
            return new EvaluationError(expectMaturityStrictlyGreaterThanSettlement(start, end));
        }
        if (isInvalidFrequency(_frequency)) {
            return new EvaluationError(expectCouponFrequencyIsValid(_frequency));
        }
        if (isInvalidDayCountConvention(_dayCountConvention)) {
            return new EvaluationError(expectDayCountConventionIsValid(_dayCountConvention));
        }
        // https://wiki.documentfoundation.org/Documentation/Calc_Functions/COUPDAYS
        if (_dayCountConvention === 1) {
            const before = COUPPCD.compute.bind(this)(settlement, maturity, frequency, dayCountConvention).value;
            const after = COUPNCD.compute.bind(this)(settlement, maturity, frequency, dayCountConvention).value;
            return toNumber(after, this.locale) - toNumber(before, this.locale);
        }
        const daysInYear = _dayCountConvention === 3 ? 365 : 360;
        return daysInYear / _frequency;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// COUPDAYBS
// -----------------------------------------------------------------------------
const COUPDAYBS = {
    description: _t("Days from settlement until next coupon."),
    args: COUPON_FUNCTION_ARGS,
    compute: function (settlement, maturity, frequency, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {
        dayCountConvention = dayCountConvention || 0;
        const start = Math.trunc(toNumber(settlement, this.locale));
        const end = Math.trunc(toNumber(maturity, this.locale));
        const _frequency = Math.trunc(toNumber(frequency, this.locale));
        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
        if (start >= end) {
            return new EvaluationError(expectMaturityStrictlyGreaterThanSettlement(start, end));
        }
        if (isInvalidFrequency(_frequency)) {
            return new EvaluationError(expectCouponFrequencyIsValid(_frequency));
        }
        if (isInvalidDayCountConvention(_dayCountConvention)) {
            return new EvaluationError(expectDayCountConventionIsValid(_dayCountConvention));
        }
        const couponBeforeStart = COUPPCD.compute.bind(this)(settlement, maturity, frequency, dayCountConvention).value;
        const _couponBeforeStart = toNumber(couponBeforeStart, this.locale);
        if ([1, 2, 3].includes(_dayCountConvention)) {
            return start - _couponBeforeStart;
        }
        if (_dayCountConvention === 4) {
            const yearFrac = getYearFrac(_couponBeforeStart, start, _dayCountConvention);
            return Math.round(yearFrac * 360);
        }
        const startDate = toJsDate(start, this.locale);
        const dateCouponBeforeStart = toJsDate(_couponBeforeStart, this.locale);
        const y1 = dateCouponBeforeStart.getFullYear();
        const y2 = startDate.getFullYear();
        const m1 = dateCouponBeforeStart.getMonth() + 1; // +1 because months in js start at 0 and it's confusing
        const m2 = startDate.getMonth() + 1;
        let d1 = dateCouponBeforeStart.getDate();
        let d2 = startDate.getDate();
        /**
         * Rules based on https://en.wikipedia.org/wiki/Day_count_convention#30/360_US
         *
         * These are slightly modified (no mention of if investment is EOM and rules order is modified),
         * but from my testing this seems the rules used by Excel/GSheet.
         */
        if (m1 === 2 &&
            m2 === 2 &&
            isLastDayOfMonth(dateCouponBeforeStart) &&
            isLastDayOfMonth(startDate)) {
            d2 = 30;
        }
        if (d2 === 31 && (d1 === 30 || d1 === 31)) {
            d2 = 30;
        }
        if (m1 === 2 && isLastDayOfMonth(dateCouponBeforeStart)) {
            d1 = 30;
        }
        if (d1 === 31) {
            d1 = 30;
        }
        return (y2 - y1) * 360 + (m2 - m1) * 30 + (d2 - d1);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// COUPDAYSNC
// -----------------------------------------------------------------------------
const COUPDAYSNC = {
    description: _t("Days from settlement until next coupon."),
    args: COUPON_FUNCTION_ARGS,
    compute: function (settlement, maturity, frequency, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {
        dayCountConvention = dayCountConvention || 0;
        const start = Math.trunc(toNumber(settlement, this.locale));
        const end = Math.trunc(toNumber(maturity, this.locale));
        const _frequency = Math.trunc(toNumber(frequency, this.locale));
        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
        if (start >= end) {
            return new EvaluationError(expectMaturityStrictlyGreaterThanSettlement(start, end));
        }
        if (isInvalidFrequency(_frequency)) {
            return new EvaluationError(expectCouponFrequencyIsValid(_frequency));
        }
        if (isInvalidDayCountConvention(_dayCountConvention)) {
            return new EvaluationError(expectDayCountConventionIsValid(_dayCountConvention));
        }
        const couponAfterStart = COUPNCD.compute.bind(this)(settlement, maturity, frequency, dayCountConvention).value;
        const _couponAfterStart = toNumber(couponAfterStart, this.locale);
        if ([1, 2, 3].includes(_dayCountConvention)) {
            return _couponAfterStart - start;
        }
        if (_dayCountConvention === 4) {
            const yearFrac = getYearFrac(start, _couponAfterStart, _dayCountConvention);
            return Math.round(yearFrac * 360);
        }
        const coupDayBs = COUPDAYBS.compute.bind(this)(settlement, maturity, frequency, dayCountConvention);
        const coupDays = COUPDAYS.compute.bind(this)(settlement, maturity, frequency, dayCountConvention);
        return toNumber(coupDays, this.locale) - toNumber(coupDayBs, this.locale);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// COUPNCD
// -----------------------------------------------------------------------------
const COUPNCD = {
    description: _t("Next coupon date after the settlement date."),
    args: COUPON_FUNCTION_ARGS,
    compute: function (settlement, maturity, frequency, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {
        dayCountConvention = dayCountConvention || 0;
        const start = Math.trunc(toNumber(settlement, this.locale));
        const end = Math.trunc(toNumber(maturity, this.locale));
        const _frequency = Math.trunc(toNumber(frequency, this.locale));
        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
        if (start >= end) {
            return new EvaluationError(expectMaturityStrictlyGreaterThanSettlement(start, end));
        }
        if (isInvalidFrequency(_frequency)) {
            return new EvaluationError(expectCouponFrequencyIsValid(_frequency));
        }
        if (isInvalidDayCountConvention(_dayCountConvention)) {
            return new EvaluationError(expectDayCountConventionIsValid(_dayCountConvention));
        }
        const monthsPerPeriod = 12 / _frequency;
        const coupNum = COUPNUM.compute.bind(this)(settlement, maturity, frequency, dayCountConvention);
        const date = addMonthsToDate(toJsDate(end, this.locale), -(toNumber(coupNum, this.locale) - 1) * monthsPerPeriod, true);
        return {
            value: jsDateToRoundNumber(date),
            format: this.locale.dateFormat,
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// COUPNUM
// -----------------------------------------------------------------------------
const COUPNUM = {
    description: _t("Number of coupons between settlement and maturity."),
    args: COUPON_FUNCTION_ARGS,
    compute: function (settlement, maturity, frequency, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {
        dayCountConvention = dayCountConvention || 0;
        const start = Math.trunc(toNumber(settlement, this.locale));
        const end = Math.trunc(toNumber(maturity, this.locale));
        const _frequency = Math.trunc(toNumber(frequency, this.locale));
        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
        if (start >= end) {
            return new EvaluationError(expectMaturityStrictlyGreaterThanSettlement(start, end));
        }
        if (isInvalidFrequency(_frequency)) {
            return new EvaluationError(expectCouponFrequencyIsValid(_frequency));
        }
        if (isInvalidDayCountConvention(_dayCountConvention)) {
            return new EvaluationError(expectDayCountConventionIsValid(_dayCountConvention));
        }
        let num = 1;
        let currentDate = end;
        const monthsPerPeriod = 12 / _frequency;
        while (currentDate > start) {
            currentDate = jsDateToRoundNumber(addMonthsToDate(toJsDate(currentDate, this.locale), -monthsPerPeriod, false));
            num++;
        }
        return num - 1;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// COUPPCD
// -----------------------------------------------------------------------------
const COUPPCD = {
    description: _t("Last coupon date prior to or on the settlement date."),
    args: COUPON_FUNCTION_ARGS,
    compute: function (settlement, maturity, frequency, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {
        dayCountConvention = dayCountConvention || 0;
        const start = Math.trunc(toNumber(settlement, this.locale));
        const end = Math.trunc(toNumber(maturity, this.locale));
        const _frequency = Math.trunc(toNumber(frequency, this.locale));
        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
        if (start >= end) {
            return new EvaluationError(expectMaturityStrictlyGreaterThanSettlement(start, end));
        }
        if (isInvalidFrequency(_frequency)) {
            return new EvaluationError(expectCouponFrequencyIsValid(_frequency));
        }
        if (isInvalidDayCountConvention(_dayCountConvention)) {
            return new EvaluationError(expectDayCountConventionIsValid(_dayCountConvention));
        }
        const monthsPerPeriod = 12 / _frequency;
        const coupNum = COUPNUM.compute.bind(this)(settlement, maturity, frequency, dayCountConvention);
        const date = addMonthsToDate(toJsDate(end, this.locale), -coupNum * monthsPerPeriod, true);
        return {
            value: jsDateToRoundNumber(date),
            format: this.locale.dateFormat,
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// CUMIPMT
// -----------------------------------------------------------------------------
const CUMIPMT = {
    description: _t("Cumulative interest paid over a set of periods."),
    args: [
        arg("rate (number)", _t("The interest rate.")),
        arg("number_of_periods (number)", _t("The number of payments to be made.")),
        arg("present_value (number)", _t("The current value of the annuity.")),
        arg("first_period (number)", _t("The number of the payment period to begin the cumulative calculation.")),
        arg("last_period (number)", _t("The number of the payment period to end the cumulative calculation.")),
        arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("The timing of the payment payments are due for each period."), PAYMENT_TIMING_OPTIONS),
    ],
    compute: function (rate, numberOfPeriods, presentValue, firstPeriod, lastPeriod, endOrBeginning = { value: DEFAULT_END_OR_BEGINNING }) {
        const first = toNumber(firstPeriod, this.locale);
        const last = toNumber(lastPeriod, this.locale);
        const r = toNumber(rate, this.locale);
        const pv = toNumber(presentValue, this.locale);
        const n = toNumber(numberOfPeriods, this.locale);
        const type = toBoolean(endOrBeginning) ? 1 : 0;
        if (n <= 0) {
            return new EvaluationError(expectNumberOfPeriodsStrictlyPositive(n));
        }
        if (first <= 0) {
            return new EvaluationError(expectFirstPeriodStrictlyPositive(first));
        }
        if (last <= 0) {
            return new EvaluationError(expectLastPeriodStrictlyPositive(last));
        }
        if (first > last) {
            return new EvaluationError(expectFirstPeriodSmallerOrEqualLastPeriod(first, last));
        }
        if (last > n) {
            return new EvaluationError(expectLastPeriodSmallerOrEqualNumberOfPeriods(last, n));
        }
        if (r <= 0) {
            return new EvaluationError(expectRateStrictlyPositive(r));
        }
        if (pv <= 0) {
            return new EvaluationError(expectPresentValueStrictlyPositive(pv));
        }
        let cumSum = 0;
        for (let i = first; i <= last; i++) {
            cumSum += impt(r, i, n, pv, 0, type);
        }
        return cumSum;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// CUMPRINC
// -----------------------------------------------------------------------------
const CUMPRINC = {
    description: _t("Cumulative principal paid over a set of periods."),
    args: [
        arg("rate (number)", _t("The interest rate.")),
        arg("number_of_periods (number)", _t("The number of payments to be made.")),
        arg("present_value (number)", _t("The current value of the annuity.")),
        arg("first_period (number)", _t("The number of the payment period to begin the cumulative calculation.")),
        arg("last_period (number)", _t("The number of the payment period to end the cumulative calculation.")),
        arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("The timing of the payment payments are due for each period."), PAYMENT_TIMING_OPTIONS),
    ],
    compute: function (rate, numberOfPeriods, presentValue, firstPeriod, lastPeriod, endOrBeginning = { value: DEFAULT_END_OR_BEGINNING }) {
        const first = toNumber(firstPeriod, this.locale);
        const last = toNumber(lastPeriod, this.locale);
        const r = toNumber(rate, this.locale);
        const pv = toNumber(presentValue, this.locale);
        const n = toNumber(numberOfPeriods, this.locale);
        const type = toBoolean(endOrBeginning) ? 1 : 0;
        if (n <= 0) {
            return new EvaluationError(expectNumberOfPeriodsStrictlyPositive(n));
        }
        if (first <= 0) {
            return new EvaluationError(expectFirstPeriodStrictlyPositive(first));
        }
        if (last <= 0) {
            return new EvaluationError(expectLastPeriodStrictlyPositive(last));
        }
        if (first > last) {
            return new EvaluationError(expectFirstPeriodSmallerOrEqualLastPeriod(first, last));
        }
        if (last > n) {
            return new EvaluationError(expectLastPeriodSmallerOrEqualNumberOfPeriods(last, n));
        }
        if (r <= 0) {
            return new EvaluationError(expectRateStrictlyPositive(r));
        }
        if (pv <= 0) {
            return new EvaluationError(expectPresentValueStrictlyPositive(pv));
        }
        let cumSum = 0;
        for (let i = first; i <= last; i++) {
            cumSum += ppmt(r, i, n, pv, 0, type);
        }
        return cumSum;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// DB
// -----------------------------------------------------------------------------
const DB = {
    description: _t("Depreciation via declining balance method."),
    args: [
        arg("cost (number)", _t("The initial cost of the asset.")),
        arg("salvage (number)", _t("The value of the asset at the end of depreciation.")),
        arg("life (number)", _t("The number of periods over which the asset is depreciated.")),
        arg("period (number)", _t("The single period within life for which to calculate depreciation.")),
        arg("month (number, optional)", _t("The number of months in the first year of depreciation.")),
    ],
    // to do: replace by dollar format
    compute: function (cost, salvage, life, period, ...args) {
        const _cost = toNumber(cost, this.locale);
        const _salvage = toNumber(salvage, this.locale);
        const _life = toNumber(life, this.locale);
        const _period = Math.trunc(toNumber(period, this.locale));
        const _month = args.length ? Math.trunc(toNumber(args[0], this.locale)) : 12;
        const lifeLimit = _life + (_month === 12 ? 0 : 1);
        if (_cost < 0) {
            return new EvaluationError(expectCostPositiveOrZero(_cost));
        }
        if (_salvage < 0) {
            return new EvaluationError(expectSalvagePositiveOrZero(_salvage));
        }
        if (_period <= 0) {
            return new EvaluationError(expectPeriodStrictlyPositive(_period));
        }
        if (_life <= 0) {
            return new EvaluationError(expectLifeStrictlyPositive(_life));
        }
        if (1 > _month || _month > 12) {
            return new EvaluationError(expectMonthBetweenOneAndTwelve(_month));
        }
        if (_period > lifeLimit) {
            return new EvaluationError(expectPeriodLessOrEqualToLifeLimit(_period, lifeLimit));
        }
        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 {
            value: before - after,
            format: "#,##0.00",
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// DDB
// -----------------------------------------------------------------------------
const DEFAULT_DDB_DEPRECIATION_FACTOR = 2;
const DEFAULT_DDB_FORMAT = "#,##0.00";
const DDB = {
    description: _t("Depreciation via double-declining balance method."),
    args: [
        arg("cost (number)", _t("The initial cost of the asset.")),
        arg("salvage (number)", _t("The value of the asset at the end of depreciation.")),
        arg("life (number)", _t("The number of periods over which the asset is depreciated.")),
        arg("period (number)", _t("The single period within life for which to calculate depreciation.")),
        arg(`factor (number, default=${DEFAULT_DDB_DEPRECIATION_FACTOR})`, _t("The factor by which depreciation decreases.")),
    ],
    compute: function (cost, salvage, life, period, factor = { value: DEFAULT_DDB_DEPRECIATION_FACTOR }) {
        const _cost = toNumber(cost, this.locale);
        const _salvage = toNumber(salvage, this.locale);
        const _life = toNumber(life, this.locale);
        const _period = toNumber(period, this.locale);
        const _factor = toNumber(factor, this.locale);
        if (_cost < 0) {
            return new EvaluationError(expectCostPositiveOrZero(_cost));
        }
        if (_salvage < 0) {
            return new EvaluationError(expectSalvagePositiveOrZero(_salvage));
        }
        if (_period <= 0) {
            return new EvaluationError(expectPeriodStrictlyPositive(_period));
        }
        if (_life <= 0) {
            return new EvaluationError(expectLifeStrictlyPositive(_life));
        }
        if (_period > _life) {
            return new EvaluationError(expectPeriodSmallerOrEqualToLife(_period, _life));
        }
        if (_factor <= 0) {
            return new EvaluationError(expectDeprecationFactorStrictlyPositive(_factor));
        }
        if (_cost === 0 || _salvage >= _cost) {
            return { value: 0, format: DEFAULT_DDB_FORMAT };
        }
        const deprecFactor = _factor / _life;
        if (deprecFactor > 1) {
            return { value: _period === 1 ? _cost - _salvage : 0, format: DEFAULT_DDB_FORMAT };
        }
        if (_period <= 1) {
            return {
                value: _cost * deprecFactor,
                format: DEFAULT_DDB_FORMAT,
            };
        }
        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 {
            value: Math.max(deprec, 0),
            format: DEFAULT_DDB_FORMAT,
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// DISC
// -----------------------------------------------------------------------------
const DISC = {
    description: _t("Discount rate of a security based on price."),
    args: [
        arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")),
        arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")),
        arg("price (number)", _t("The price at which the security is bought per 100 face value.")),
        arg("redemption (number)", _t("The redemption amount per 100 face value, or par.")),
        arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use."), DAY_COUNT_CONVENTION_OPTIONS),
    ],
    compute: function (settlement, maturity, price, redemption, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {
        dayCountConvention = dayCountConvention || 0;
        const _settlement = Math.trunc(toNumber(settlement, this.locale));
        const _maturity = Math.trunc(toNumber(maturity, this.locale));
        const _price = toNumber(price, this.locale);
        const _redemption = toNumber(redemption, this.locale);
        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
        if (_settlement >= _maturity) {
            return new EvaluationError(expectMaturityStrictlyGreaterThanSettlement(_settlement, _maturity));
        }
        if (isInvalidDayCountConvention(_dayCountConvention)) {
            return new EvaluationError(expectDayCountConventionIsValid(_dayCountConvention));
        }
        if (_price <= 0) {
            return new EvaluationError(expectPriceStrictlyPositive(_price));
        }
        if (_redemption <= 0) {
            return new EvaluationError(expectRedemptionStrictlyPositive(_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 = getYearFrac(_settlement, _maturity, _dayCountConvention);
        return (_redemption - _price) / _redemption / yearsFrac;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// DOLLARDE
// -----------------------------------------------------------------------------
const DOLLARDE = {
    description: _t("Convert a decimal fraction to decimal value."),
    args: [
        arg("fractional_price (number)", _t("The price quotation given using fractional decimal conventions.")),
        arg("unit (number)", _t("The units of the fraction, e.g. 8 for 1/8ths or 32 for 1/32nds.")),
    ],
    compute: function (fractionalPrice, unit) {
        const price = toNumber(fractionalPrice, this.locale);
        const _unit = Math.trunc(toNumber(unit, this.locale));
        if (_unit <= 0) {
            return new EvaluationError(expectUnitStrictlyPositive(_unit));
        }
        const truncatedPrice = Math.trunc(price);
        const priceFractionalPart = price - truncatedPrice;
        const frac = 10 ** Math.ceil(Math.log10(_unit)) / _unit;
        return truncatedPrice + priceFractionalPart * frac;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// DOLLARFR
// -----------------------------------------------------------------------------
const DOLLARFR = {
    description: _t("Convert a decimal value to decimal fraction."),
    args: [
        arg("decimal_price (number)", _t("The price quotation given as a decimal value.")),
        arg("unit (number)", _t("The units of the desired fraction, e.g. 8 for 1/8ths or 32 for 1/32nds.")),
    ],
    compute: function (decimalPrice, unit) {
        const price = toNumber(decimalPrice, this.locale);
        const _unit = Math.trunc(toNumber(unit, this.locale));
        if (_unit <= 0) {
            return new EvaluationError(expectUnitStrictlyPositive(_unit));
        }
        const truncatedPrice = Math.trunc(price);
        const priceFractionalPart = price - truncatedPrice;
        const frac = _unit / 10 ** Math.ceil(Math.log10(_unit));
        return truncatedPrice + priceFractionalPart * frac;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// DURATION
// -----------------------------------------------------------------------------
const DURATION = {
    description: _t("Number of periods for an investment to reach a value."),
    args: [
        arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")),
        arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")),
        arg("rate (number)", _t("The annualized rate of interest.")),
        arg("yield (number)", _t("The expected annual yield of the security.")),
        arg("frequency (number)", _t("The number of interest or coupon payments per year (1, 2, or 4)."), FREQUENCY_OPTIONS),
        arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use."), DAY_COUNT_CONVENTION_OPTIONS),
    ],
    compute: function (settlement, maturity, rate, securityYield, frequency, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {
        const start = Math.trunc(toNumber(settlement, this.locale));
        const end = Math.trunc(toNumber(maturity, this.locale));
        const _rate = toNumber(rate, this.locale);
        const _yield = toNumber(securityYield, this.locale);
        const _frequency = Math.trunc(toNumber(frequency, this.locale));
        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
        if (start >= end) {
            return new EvaluationError(expectMaturityStrictlyGreaterThanSettlement(start, end));
        }
        if (isInvalidFrequency(_frequency)) {
            return new EvaluationError(expectCouponFrequencyIsValid(_frequency));
        }
        if (isInvalidDayCountConvention(_dayCountConvention)) {
            return new EvaluationError(expectDayCountConventionIsValid(_dayCountConvention));
        }
        if (_rate < 0) {
            return new EvaluationError(expectRatePositiveOrZero(_rate));
        }
        if (_yield < 0) {
            return new EvaluationError(expectYieldPositiveOrZero(_yield));
        }
        const years = getYearFrac(start, end, _dayCountConvention);
        const timeFirstYear = years - Math.trunc(years) || 1 / _frequency;
        const nbrCoupons = Math.ceil(years * _frequency);
        // The DURATION function return the Macaulay duration
        // See example: https://en.wikipedia.org/wiki/Bond_duration#Formulas
        const cashFlowFromCoupon = _rate / _frequency;
        const yieldPerPeriod = _yield / _frequency;
        let count = 0;
        let sum = 0;
        for (let i = 1; i <= nbrCoupons; i++) {
            const cashFlowPerPeriod = cashFlowFromCoupon + (i === nbrCoupons ? 1 : 0);
            const presentValuePerPeriod = cashFlowPerPeriod / (1 + yieldPerPeriod) ** i;
            sum += (timeFirstYear + (i - 1) / _frequency) * presentValuePerPeriod;
            count += presentValuePerPeriod;
        }
        return count === 0 ? 0 : sum / count;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// EFFECT
// -----------------------------------------------------------------------------
const EFFECT = {
    description: _t("Annual effective interest rate."),
    args: [
        arg("nominal_rate (number)", _t("The nominal interest rate per year.")),
        arg("periods_per_year (number)", _t("The number of compounding periods per year.")),
    ],
    compute: function (nominal_rate, periods_per_year) {
        const nominal = toNumber(nominal_rate, this.locale);
        const periods = Math.trunc(toNumber(periods_per_year, this.locale));
        if (nominal <= 0) {
            return new EvaluationError(expectNominalRateStrictlyPositive(nominal));
        }
        if (periods <= 0) {
            return new EvaluationError(expectPeriodsByYearStrictlyPositive(periods));
        }
        // 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;
function fv(r, n, p, pv, t) {
    if (r === 0) {
        return -(pv + p * n);
    }
    return -pv * (1 + r) ** n - (p * (1 + r * t) * ((1 + r) ** n - 1)) / r;
}
const FV = {
    description: _t("Future value of an annuity investment."),
    args: [
        arg("rate (number)", _t("The interest rate.")),
        arg("number_of_periods (number)", _t("The number of payments to be made.")),
        arg("payment_amount (number)", _t("The amount per period to be paid.")),
        arg(`present_value (number, default=${DEFAULT_PRESENT_VALUE})`, _t("The current value of the annuity.")),
        arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("The timing of the payment payments are due for each period."), PAYMENT_TIMING_OPTIONS),
    ],
    // to do: replace by dollar format
    compute: function (rate, numberOfPeriods, paymentAmount, presentValue = { value: DEFAULT_PRESENT_VALUE }, endOrBeginning = { value: DEFAULT_END_OR_BEGINNING }) {
        presentValue = presentValue || 0;
        endOrBeginning = endOrBeginning || 0;
        const r = toNumber(rate, this.locale);
        const n = toNumber(numberOfPeriods, this.locale);
        const p = toNumber(paymentAmount, this.locale);
        const pv = toNumber(presentValue, this.locale);
        const type = toBoolean(endOrBeginning) ? 1 : 0;
        return {
            value: fv(r, n, p, pv, type),
            format: "#,##0.00",
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// FVSCHEDULE
// -----------------------------------------------------------------------------
const FVSCHEDULE = {
    description: _t("Future value of principal from series of rates."),
    args: [
        arg("principal (number)", _t("The amount of initial capital or value to compound against.")),
        arg("rate_schedule (number, range<number>)", _t("A series of interest rates to compound against the principal.")),
    ],
    compute: function (principalAmount, rateSchedule) {
        const principal = toNumber(principalAmount, this.locale);
        return reduceAny([rateSchedule], (acc, rate) => acc * (1 + toNumber(rate, this.locale)), principal);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// INTRATE
// -----------------------------------------------------------------------------
const INTRATE = {
    description: _t("Calculates effective interest rate."),
    args: [
        arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")),
        arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")),
        arg("investment (number)", _t("The amount invested in the security.")),
        arg("redemption (number)", _t("The amount to be received at maturity.")),
        arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use."), DAY_COUNT_CONVENTION_OPTIONS),
    ],
    compute: function (settlement, maturity, investment, redemption, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {
        const _settlement = Math.trunc(toNumber(settlement, this.locale));
        const _maturity = Math.trunc(toNumber(maturity, this.locale));
        const _redemption = toNumber(redemption, this.locale);
        const _investment = toNumber(investment, this.locale);
        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
        if (_settlement >= _maturity) {
            return new EvaluationError(expectMaturityStrictlyGreaterThanSettlement(_settlement, _maturity));
        }
        if (_investment <= 0) {
            return new EvaluationError(expectInvestmentStrictlyPositive(_investment));
        }
        if (_redemption <= 0) {
            return new EvaluationError(expectRedemptionStrictlyPositive(_redemption));
        }
        if (isInvalidDayCountConvention(_dayCountConvention)) {
            return new EvaluationError(expectDayCountConventionIsValid(_dayCountConvention));
        }
        /**
         * https://wiki.documentfoundation.org/Documentation/Calc_Functions/INTRATE
         *
         *             (Redemption  - Investment) / Investment
         * INTRATE =  _________________________________________
         *              YEARFRAC(settlement, maturity, basis)
         */
        const yearFrac = getYearFrac(_settlement, _maturity, _dayCountConvention);
        return (_redemption - _investment) / _investment / yearFrac;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// IPMT
// -----------------------------------------------------------------------------
function impt(r, per, n, pv, fv, type) {
    return pmt(r, n, pv, fv, type) - ppmt(r, per, n, pv, fv, type);
}
const IPMT = {
    description: _t("Payment on the principal of an investment."),
    args: [
        arg("rate (number)", _t("The annualized rate of interest.")),
        arg("period (number)", _t("The amortization period, in terms of number of periods.")),
        arg("number_of_periods (number)", _t("The number of payments to be made.")),
        arg("present_value (number)", _t("The current value of the annuity.")),
        arg(`future_value (number, default=${DEFAULT_FUTURE_VALUE})`, _t("The future value remaining after the final payment has been made.")),
        arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("The timing of the payment payments are due for each period."), PAYMENT_TIMING_OPTIONS),
    ],
    compute: function (rate, currentPeriod, numberOfPeriods, presentValue, futureValue = { value: DEFAULT_FUTURE_VALUE }, endOrBeginning = { value: DEFAULT_END_OR_BEGINNING }) {
        const r = toNumber(rate, this.locale);
        const period = toNumber(currentPeriod, this.locale);
        const n = toNumber(numberOfPeriods, this.locale);
        const pv = toNumber(presentValue, this.locale);
        const fv = toNumber(futureValue, this.locale);
        const type = toBoolean(endOrBeginning) ? 1 : 0;
        return {
            value: impt(r, period, n, pv, fv, type),
            format: "#,##0.00",
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// IRR
// -----------------------------------------------------------------------------
const DEFAULT_RATE_GUESS = 0.1;
const IRR = {
    description: _t("Internal rate of return given periodic cashflows."),
    args: [
        arg("cashflow_amounts (number, range<number>)", _t("An array or range containing the income or payments associated with the investment.")),
        arg(`rate_guess (number, default=${DEFAULT_RATE_GUESS})`, _t("An estimate for what the internal rate of return will be.")),
    ],
    compute: function (cashFlowAmounts, rateGuess = { value: DEFAULT_RATE_GUESS }) {
        const _rateGuess = toNumber(rateGuess, this.locale);
        if (_rateGuess <= -1) {
            return new EvaluationError(expectRateGuessStrictlyGreaterThanMinusOne(_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;
        const amounts = [];
        visitNumbers([cashFlowAmounts], ({ value: amount }) => {
            if (amount > 0)
                positive = true;
            if (amount < 0)
                negative = true;
            amounts.push(amount);
        }, this.locale);
        if (!positive || !negative) {
            return new EvaluationError(expectCashFlowsHavePositiveAndNegativesValues);
        }
        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 {
            value: newtonMethod(func, derivFunc, _rateGuess + 1, 20, 1e-5) - 1,
            format: "0%",
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// ISPMT
// -----------------------------------------------------------------------------
const ISPMT = {
    description: _t("Returns the interest paid at a particular period of an investment."),
    args: [
        arg("rate (number)", _t("The interest rate.")),
        arg("period (number)", _t("The period for which you want to view the interest payment.")),
        arg("number_of_periods (number)", _t("The number of payments to be made.")),
        arg("present_value (number)", _t("The current value of the annuity.")),
    ],
    compute: function (rate, currentPeriod, numberOfPeriods, presentValue) {
        const interestRate = toNumber(rate, this.locale);
        const period = toNumber(currentPeriod, this.locale);
        const nOfPeriods = toNumber(numberOfPeriods, this.locale);
        const investment = toNumber(presentValue, this.locale);
        if (nOfPeriods === 0) {
            return new EvaluationError(expectNumberOfPeriodDifferentFromZero(nOfPeriods));
        }
        const currentInvestment = investment - investment * (period / nOfPeriods);
        return -1 * currentInvestment * interestRate;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// MDURATION
// -----------------------------------------------------------------------------
const MDURATION = {
    description: _t("Modified Macaulay duration."),
    args: [
        arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")),
        arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")),
        arg("rate (number)", _t("The annualized rate of interest.")),
        arg("yield (number)", _t("The expected annual yield of the security.")),
        arg("frequency (number)", _t("The number of interest or coupon payments per year (1, 2, or 4)."), FREQUENCY_OPTIONS),
        arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")),
    ],
    compute: function (settlement, maturity, rate, securityYield, frequency, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {
        const duration = DURATION.compute.bind(this)(settlement, maturity, rate, securityYield, frequency, dayCountConvention);
        const y = toNumber(securityYield, this.locale);
        const k = Math.trunc(toNumber(frequency, this.locale));
        return toNumber(duration, this.locale) / (1 + y / k);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// MIRR
// -----------------------------------------------------------------------------
const MIRR = {
    description: _t("Modified internal rate of return."),
    args: [
        arg("cashflow_amounts (range<number>)", _t("A range containing the income or payments associated with the investment. The array should contain bot payments and incomes.")),
        arg("financing_rate (number)", _t("The interest rate paid on funds invested.")),
        arg("reinvestment_return_rate (number)", _t("The return (as a percentage) earned on reinvestment of income received from the investment.")),
    ],
    compute: function (cashflowAmount, financingRate, reinvestmentRate) {
        const fRate = toNumber(financingRate, this.locale);
        const rRate = toNumber(reinvestmentRate, this.locale);
        const cashFlow = transposeMatrix(cashflowAmount)
            .flat()
            .filter((t) => t.value !== null)
            .map((val) => toNumber(val, this.locale));
        const n = cashFlow.length;
        /**
         * https://en.wikipedia.org/wiki/Modified_internal_rate_of_return
         *
         *         /  FV(positive cash flows, reinvestment rate) \  ^ (1 / (n - 1))
         * MIRR = |  ___________________________________________  |                 - 1
         *         \   - PV(negative cash flows, finance rate)   /
         *
         * with n the number of cash flows.
         *
         * You can compute FV and PV as :
         *
         * FV =    SUM      [ (cashFlow[i]>0 ? cashFlow[i] : 0) * (1 + rRate)**(n - i-1) ]
         *       i= 0 => n
         *
         * PV =    SUM      [ (cashFlow[i]<0 ? cashFlow[i] : 0) / (1 + fRate)**i ]
         *       i= 0 => n
         */
        let fv = 0;
        let pv = 0;
        for (const i of range(0, n)) {
            const amount = cashFlow[i];
            if (amount >= 0) {
                fv += amount * (rRate + 1) ** (n - i - 1);
            }
            else {
                pv += amount / (fRate + 1) ** i;
            }
        }
        if (pv === 0 || fv === 0) {
            return new EvaluationError(expectCashFlowsHavePositiveAndNegativesValues);
        }
        const exponent = 1 / (n - 1);
        return (-fv / pv) ** exponent - 1;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// NOMINAL
// -----------------------------------------------------------------------------
const NOMINAL = {
    description: _t("Annual nominal interest rate."),
    args: [
        arg("effective_rate (number)", _t("The effective interest rate per year.")),
        arg("periods_per_year (number)", _t("The number of compounding periods per year.")),
    ],
    compute: function (effective_rate, periods_per_year) {
        const effective = toNumber(effective_rate, this.locale);
        const periods = Math.trunc(toNumber(periods_per_year, this.locale));
        if (effective <= 0) {
            return new EvaluationError(expectEffectiveRateStrictlyPositive(effective));
        }
        if (periods <= 0) {
            return new EvaluationError(expectPeriodsByYearStrictlyPositive(periods));
        }
        // https://en.wikipedia.org/wiki/Nominal_interest_rate#Nominal_versus_effective_interest_rate
        return (Math.pow(effective + 1, 1 / periods) - 1) * periods;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// NPER
// -----------------------------------------------------------------------------
const NPER = {
    description: _t("Number of payment periods for an investment."),
    args: [
        arg("rate (number)", _t("The interest rate.")),
        arg("payment_amount (number)", _t("The amount of each payment made.")),
        arg("present_value (number)", _t("The current value of the annuity.")),
        arg(`future_value (number, default=${DEFAULT_FUTURE_VALUE})`, _t("The future value remaining after the final payment has been made.")),
        arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("The timing of the payment payments are due for each period."), PAYMENT_TIMING_OPTIONS),
    ],
    compute: function (rate, paymentAmount, presentValue, futureValue = { value: DEFAULT_FUTURE_VALUE }, endOrBeginning = { value: DEFAULT_END_OR_BEGINNING }) {
        futureValue = futureValue || 0;
        endOrBeginning = endOrBeginning || 0;
        const r = toNumber(rate, this.locale);
        const p = toNumber(paymentAmount, this.locale);
        const pv = toNumber(presentValue, this.locale);
        const fv = toNumber(futureValue, this.locale);
        const t = toBoolean(endOrBeginning) ? 1 : 0;
        /**
         * https://wiki.documentfoundation.org/Documentation/Calc_Functions/NPER
         *
         * 0 = pv * (1 + r)^N + fv + [ p * (1 + r * t) * ((1 + r)^N - 1) ] / r
         *
         * We solve the equation for N:
         *
         * with C = [ p * (1 + r * t)] / r and
         *      R = 1 + r
         *
         * => 0 = pv * R^N + C * R^N - C + fv
         * <=> (C - fv) = R^N * (pv + C)
         * <=> log[(C - fv) / (pv + C)] = N * log(R)
         */
        if (r === 0) {
            return -(fv + pv) / p;
        }
        const c = (p * (1 + r * t)) / r;
        return Math.log((c - fv) / (pv + c)) / Math.log(1 + r);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// NPV
// -----------------------------------------------------------------------------
function npvResult(r, startValue, values, locale) {
    let i = 0;
    return reduceNumbers(values, (acc, v) => {
        i++;
        return acc + v / (1 + r) ** i;
    }, startValue, locale);
}
const NPV = {
    description: _t("The net present value of an investment based on a series of periodic cash flows and a discount rate."),
    args: [
        arg("discount (number)", _t("The discount rate of the investment over one period.")),
        arg("cashflow1 (number, range<number>)", _t("The first future cash flow.")),
        arg("cashflow2 (number, range<number>, repeating)", _t("Additional future cash flows.")),
    ],
    // to do: replace by dollar format
    compute: function (discount, ...values) {
        const _discount = toNumber(discount, this.locale);
        if (_discount === -1) {
            return new EvaluationError(expectDiscountDifferentFromMinusOne(_discount));
        }
        return {
            value: npvResult(_discount, 0, values, this.locale),
            format: "#,##0.00",
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// PDURATION
// -----------------------------------------------------------------------------
const PDURATION = {
    description: _t("Computes the number of periods needed for an investment to reach a value."),
    args: [
        arg("rate (number)", _t("The rate at which the investment grows each period.")),
        arg("present_value (number)", _t("The investment's current value.")),
        arg("future_value (number)", _t("The investment's desired future value.")),
    ],
    compute: function (rate, presentValue, futureValue) {
        const _rate = toNumber(rate, this.locale);
        const _presentValue = toNumber(presentValue, this.locale);
        const _futureValue = toNumber(futureValue, this.locale);
        if (_rate <= 0) {
            return new EvaluationError(expectRateStrictlyPositive(_rate));
        }
        if (_presentValue <= 0) {
            return new EvaluationError(expectPresentValueStrictlyPositive(_presentValue));
        }
        if (_futureValue <= 0) {
            return new EvaluationError(expectFutureValueStrictlyPositive(_futureValue));
        }
        return (Math.log(_futureValue) - Math.log(_presentValue)) / Math.log(1 + _rate);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// PMT
// -----------------------------------------------------------------------------
function pmt(r, n, pv, fv, t) {
    if (n <= 0) {
        throw new EvaluationError(expectNumberOfPeriodsStrictlyPositive(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;
}
const PMT = {
    description: _t("Periodic payment for an annuity investment."),
    args: [
        arg("rate (number)", _t("The annualized rate of interest.")),
        arg("number_of_periods (number)", _t("The number of payments to be made.")),
        arg("present_value (number)", _t("The current value of the annuity.")),
        arg(`future_value (number, default=${DEFAULT_FUTURE_VALUE})`, _t("The future value remaining after the final payment has been made.")),
        arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("The timing of the payment payments are due for each period."), PAYMENT_TIMING_OPTIONS),
    ],
    compute: function (rate, numberOfPeriods, presentValue, futureValue = { value: DEFAULT_FUTURE_VALUE }, endOrBeginning = { value: DEFAULT_END_OR_BEGINNING }) {
        const n = toNumber(numberOfPeriods, this.locale);
        const r = toNumber(rate, this.locale);
        const t = toBoolean(endOrBeginning) ? 1 : 0;
        const fv = toNumber(futureValue, this.locale);
        const pv = toNumber(presentValue, this.locale);
        return { value: pmt(r, n, pv, fv, t), format: "#,##0.00" };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// PPMT
// -----------------------------------------------------------------------------
function ppmt(r, per, n, pValue, fValue, t) {
    if (n <= 0) {
        throw new EvaluationError(expectNumberOfPeriodsStrictlyPositive(n));
    }
    if (per <= 0 || per > n) {
        throw new EvaluationError(expectPeriodBetweenOneAndNumberOfPeriods(n));
    }
    const payment = pmt(r, n, pValue, fValue, t);
    if (t === 1 && per === 1)
        return payment;
    const eqPeriod = t === 0 ? per - 1 : per - 2;
    const eqPv = pValue + payment * t;
    const capitalAtPeriod = -fv(r, eqPeriod, payment, eqPv, 0);
    const currentInterest = capitalAtPeriod * r;
    return payment + currentInterest;
}
const PPMT = {
    description: _t("Payment on the principal of an investment."),
    args: [
        arg("rate (number)", _t("The annualized rate of interest.")),
        arg("period (number)", _t("The amortization period, in terms of number of periods.")),
        arg("number_of_periods (number)", _t("The number of payments to be made.")),
        arg("present_value (number)", _t("The current value of the annuity.")),
        arg(`future_value (number, default=${DEFAULT_FUTURE_VALUE})`, _t("The future value remaining after the final payment has been made.")),
        arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("The timing of the payment payments are due for each period."), PAYMENT_TIMING_OPTIONS),
    ],
    compute: function (rate, currentPeriod, numberOfPeriods, presentValue, futureValue = { value: DEFAULT_FUTURE_VALUE }, endOrBeginning = { value: DEFAULT_END_OR_BEGINNING }) {
        const n = toNumber(numberOfPeriods, this.locale);
        const r = toNumber(rate, this.locale);
        const period = toNumber(currentPeriod, this.locale);
        const type = toBoolean(endOrBeginning) ? 1 : 0;
        const fv = toNumber(futureValue, this.locale);
        const pv = toNumber(presentValue, this.locale);
        return {
            value: ppmt(r, period, n, pv, fv, type),
            format: "#,##0.00",
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// PV
// -----------------------------------------------------------------------------
const PV = {
    description: _t("Present value of an annuity investment."),
    args: [
        arg("rate (number)", _t("The interest rate.")),
        arg("number_of_periods (number)", _t("The number of payments to be made.")),
        arg("payment_amount (number)", _t("The amount per period to be paid.")),
        arg(`future_value (number, default=${DEFAULT_FUTURE_VALUE})`, _t("The future value remaining after the final payment has been made.")),
        arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("The timing of the payment payments are due for each period."), PAYMENT_TIMING_OPTIONS),
    ],
    // to do: replace by dollar format
    compute: function (rate, numberOfPeriods, paymentAmount, futureValue = { value: DEFAULT_FUTURE_VALUE }, endOrBeginning = { value: DEFAULT_END_OR_BEGINNING }) {
        futureValue = futureValue || 0;
        endOrBeginning = endOrBeginning || 0;
        const r = toNumber(rate, this.locale);
        const n = toNumber(numberOfPeriods, this.locale);
        const p = toNumber(paymentAmount, this.locale);
        const fv = toNumber(futureValue, this.locale);
        const type = toBoolean(endOrBeginning) ? 1 : 0;
        // https://wiki.documentfoundation.org/Documentation/Calc_Functions/PV
        return {
            value: r
                ? -((p * (1 + r * type) * ((1 + r) ** n - 1)) / r + fv) / (1 + r) ** n
                : -(fv + p * n),
            format: "#,##0.00",
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// PRICE
// -----------------------------------------------------------------------------
const PRICE = {
    description: _t("Price of a security paying periodic interest."),
    args: [
        arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")),
        arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")),
        arg("rate (number)", _t("The annualized rate of interest.")),
        arg("yield (number)", _t("The expected annual yield of the security.")),
        arg("redemption (number)", _t("The redemption amount per 100 face value, or par.")),
        arg("frequency (number)", _t("The number of interest or coupon payments per year (1, 2, or 4)."), FREQUENCY_OPTIONS),
        arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use."), DAY_COUNT_CONVENTION_OPTIONS),
    ],
    compute: function (settlement, maturity, rate, securityYield, redemption, frequency, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {
        dayCountConvention = dayCountConvention || 0;
        const _settlement = Math.trunc(toNumber(settlement, this.locale));
        const _maturity = Math.trunc(toNumber(maturity, this.locale));
        const _rate = toNumber(rate, this.locale);
        const _yield = toNumber(securityYield, this.locale);
        const _redemption = toNumber(redemption, this.locale);
        const _frequency = Math.trunc(toNumber(frequency, this.locale));
        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
        if (_settlement >= _maturity) {
            return new EvaluationError(expectMaturityStrictlyGreaterThanSettlement(_settlement, _maturity));
        }
        if (isInvalidFrequency(_frequency)) {
            return new EvaluationError(expectCouponFrequencyIsValid(_frequency));
        }
        if (isInvalidDayCountConvention(_dayCountConvention)) {
            return new EvaluationError(expectDayCountConventionIsValid(_dayCountConvention));
        }
        if (_rate < 0) {
            return new EvaluationError(expectRatePositiveOrZero(_rate));
        }
        if (_yield < 0) {
            return new EvaluationError(expectYieldPositiveOrZero(_yield));
        }
        if (_redemption <= 0) {
            return new EvaluationError(expectRedemptionStrictlyPositive(_redemption));
        }
        const years = getYearFrac(_settlement, _maturity, _dayCountConvention);
        const nbrRealCoupons = years * _frequency;
        const nbrFullCoupons = Math.ceil(nbrRealCoupons);
        const timeFirstCoupon = nbrRealCoupons - Math.floor(nbrRealCoupons) || 1;
        const yieldFactorPerPeriod = 1 + _yield / _frequency;
        const cashFlowFromCoupon = (100 * _rate) / _frequency;
        if (nbrFullCoupons === 1) {
            return ((cashFlowFromCoupon + _redemption) / ((timeFirstCoupon * _yield) / _frequency + 1) -
                cashFlowFromCoupon * (1 - timeFirstCoupon));
        }
        let cashFlowsPresentValue = 0;
        for (let i = 1; i <= nbrFullCoupons; i++) {
            cashFlowsPresentValue +=
                cashFlowFromCoupon / yieldFactorPerPeriod ** (i - 1 + timeFirstCoupon);
        }
        const redemptionPresentValue = _redemption / yieldFactorPerPeriod ** (nbrFullCoupons - 1 + timeFirstCoupon);
        return (redemptionPresentValue + cashFlowsPresentValue - cashFlowFromCoupon * (1 - timeFirstCoupon));
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// PRICEDISC
// -----------------------------------------------------------------------------
const PRICEDISC = {
    description: _t("Price of a discount security."),
    args: [
        arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")),
        arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")),
        arg("discount (number)", _t("The discount rate of the security at time of purchase.")),
        arg("redemption (number)", _t("The redemption amount per 100 face value, or par.")),
        arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use."), DAY_COUNT_CONVENTION_OPTIONS),
    ],
    compute: function (settlement, maturity, discount, redemption, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {
        dayCountConvention = dayCountConvention || 0;
        const _settlement = Math.trunc(toNumber(settlement, this.locale));
        const _maturity = Math.trunc(toNumber(maturity, this.locale));
        const _discount = toNumber(discount, this.locale);
        const _redemption = toNumber(redemption, this.locale);
        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
        if (_settlement >= _maturity) {
            return new EvaluationError(expectMaturityStrictlyGreaterThanSettlement(_settlement, _maturity));
        }
        if (isInvalidDayCountConvention(_dayCountConvention)) {
            return new EvaluationError(expectDayCountConventionIsValid(_dayCountConvention));
        }
        if (_discount <= 0) {
            return new EvaluationError(expectDiscountStrictlyPositive(_discount));
        }
        if (_redemption <= 0) {
            return new EvaluationError(expectRedemptionStrictlyPositive(_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 = getYearFrac(_settlement, _maturity, _dayCountConvention);
        return _redemption - _discount * _redemption * yearsFrac;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// PRICEMAT
// -----------------------------------------------------------------------------
const PRICEMAT = {
    description: _t("Calculates the price of a security paying interest at maturity, based on expected yield."),
    args: [
        arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")),
        arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")),
        arg("issue (date)", _t("The date the security was initially issued.")),
        arg("rate (number)", _t("The annualized rate of interest.")),
        arg("yield (number)", _t("The expected annual yield of the security.")),
        arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use."), DAY_COUNT_CONVENTION_OPTIONS),
    ],
    compute: function (settlement, maturity, issue, rate, securityYield, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {
        dayCountConvention = dayCountConvention || 0;
        const _settlement = Math.trunc(toNumber(settlement, this.locale));
        const _maturity = Math.trunc(toNumber(maturity, this.locale));
        const _issue = Math.trunc(toNumber(issue, this.locale));
        const _rate = toNumber(rate, this.locale);
        const _yield = toNumber(securityYield, this.locale);
        const _dayCount = Math.trunc(toNumber(dayCountConvention, this.locale));
        if (_settlement <= _issue) {
            return new EvaluationError(expectSettlementStrictlyGreaterThanIssue(_settlement, _issue));
        }
        if (_settlement >= _maturity) {
            return new EvaluationError(expectMaturityStrictlyGreaterThanSettlement(_settlement, _maturity));
        }
        if (isInvalidDayCountConvention(_dayCount)) {
            return new EvaluationError(expectDayCountConventionIsValid(_dayCount));
        }
        if (_rate < 0) {
            return new EvaluationError(expectRatePositiveOrZero(_rate));
        }
        if (_yield < 0) {
            return new EvaluationError(expectYieldPositiveOrZero(_yield));
        }
        /**
         * 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 = getYearFrac(_settlement, _maturity, _dayCount);
        const issueToSettlement = getYearFrac(_settlement, _issue, _dayCount);
        const issueToMaturity = getYearFrac(_issue, _maturity, _dayCount);
        const numerator = 100 + issueToMaturity * _rate * 100;
        const denominator = 1 + settlementToMaturity * _yield;
        const term2 = issueToSettlement * _rate * 100;
        return numerator / denominator - term2;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// RATE
// -----------------------------------------------------------------------------
const RATE_GUESS_DEFAULT = 0.1;
const RATE = {
    description: _t("Interest rate of an annuity investment."),
    args: [
        arg("number_of_periods (number)", _t("The number of payments to be made.")),
        arg("payment_per_period (number)", _t("The amount per period to be paid.")),
        arg("present_value (number)", _t("The current value of the annuity.")),
        arg(`future_value (number, default=${DEFAULT_FUTURE_VALUE})`, _t("The future value remaining after the final payment has been made.")),
        arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("The timing of the payment payments are due for each period."), PAYMENT_TIMING_OPTIONS),
        arg(`rate_guess (number, default=${RATE_GUESS_DEFAULT})`, _t("An estimate for what the interest rate will be.")),
    ],
    compute: function (numberOfPeriods, paymentPerPeriod, presentValue, futureValue = { value: DEFAULT_FUTURE_VALUE }, endOrBeginning = { value: DEFAULT_END_OR_BEGINNING }, rateGuess = { value: RATE_GUESS_DEFAULT }) {
        const n = toNumber(numberOfPeriods, this.locale);
        const payment = toNumber(paymentPerPeriod, this.locale);
        const type = toBoolean(endOrBeginning) ? 1 : 0;
        const guess = toNumber(rateGuess, this.locale) || RATE_GUESS_DEFAULT;
        let fv = toNumber(futureValue, this.locale);
        let pv = toNumber(presentValue, this.locale);
        if (n <= 0) {
            return new EvaluationError(expectNumberOfPeriodsStrictlyPositive(n));
        }
        if (!havePositiveAndNegativeValues([payment, pv, fv])) {
            return new EvaluationError(_t("There must be both positive and negative values in [payment_amount, present_value, future_value]."));
        }
        if (guess <= -1) {
            return new EvaluationError(expectRateGuessStrictlyGreaterThanMinusOne(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 {
            value: newtonMethod(func, derivFunc, guess, 40, 1e-5),
            format: "0%",
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// RECEIVED
// -----------------------------------------------------------------------------
const RECEIVED = {
    description: _t("Amount received at maturity for a security."),
    args: [
        arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")),
        arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")),
        arg("investment (number)", _t("The amount invested (irrespective of face value of each security).")),
        arg("discount (number)", _t("The discount rate of the security invested in.")),
        arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use."), DAY_COUNT_CONVENTION_OPTIONS),
    ],
    compute: function (settlement, maturity, investment, discount, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {
        dayCountConvention = dayCountConvention || 0;
        const _settlement = Math.trunc(toNumber(settlement, this.locale));
        const _maturity = Math.trunc(toNumber(maturity, this.locale));
        const _investment = toNumber(investment, this.locale);
        const _discount = toNumber(discount, this.locale);
        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
        if (_settlement >= _maturity) {
            return new EvaluationError(expectMaturityStrictlyGreaterThanSettlement(_settlement, _maturity));
        }
        if (isInvalidDayCountConvention(_dayCountConvention)) {
            return new EvaluationError(expectDayCountConventionIsValid(_dayCountConvention));
        }
        if (_investment <= 0) {
            return new EvaluationError(expectInvestmentStrictlyPositive(_investment));
        }
        if (_discount <= 0) {
            return new EvaluationError(expectDiscountStrictlyPositive(_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 = getYearFrac(_settlement, _maturity, _dayCountConvention);
        return _investment / (1 - _discount * yearsFrac);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// RRI
// -----------------------------------------------------------------------------
const RRI = {
    description: _t("Computes the rate needed for an investment to reach a specific value within a specific number of periods."),
    args: [
        arg("number_of_periods (number)", _t("The number of periods.")),
        arg("present_value (number)", _t("The present value of the investment.")),
        arg("future_value (number)", _t("The future value of the investment.")),
    ],
    compute: function (numberOfPeriods, presentValue, futureValue) {
        const n = toNumber(numberOfPeriods, this.locale);
        const pv = toNumber(presentValue, this.locale);
        const fv = toNumber(futureValue, this.locale);
        if (n <= 0) {
            return new EvaluationError(expectNumberOfPeriodsStrictlyPositive(n));
        }
        /**
         * https://support.microsoft.com/en-us/office/rri-function-6f5822d8-7ef1-4233-944c-79e8172930f4
         *
         * RRI = (future value / present value) ^ (1 / number of periods) - 1
         */
        return (fv / pv) ** (1 / n) - 1;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// SLN
// -----------------------------------------------------------------------------
const SLN = {
    description: _t("Depreciation of an asset using the straight-line method."),
    args: [
        arg("cost (number)", _t("The initial cost of the asset.")),
        arg("salvage (number)", _t("The value of the asset at the end of depreciation.")),
        arg("life (number)", _t("The number of periods over which the asset is depreciated.")),
    ],
    compute: function (cost, salvage, life) {
        const _cost = toNumber(cost, this.locale);
        const _salvage = toNumber(salvage, this.locale);
        const _life = toNumber(life, this.locale);
        // No assertion is done on the values of the arguments to be compatible with Excel/Gsheet that don't check the values.
        // It's up to the user to make sure the arguments make sense, which is good design because the user is smart.
        return {
            value: (_cost - _salvage) / _life,
            format: "#,##0.00",
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// SYD
// -----------------------------------------------------------------------------
const SYD = {
    description: _t("Depreciation via sum of years digit method."),
    args: [
        arg("cost (number)", _t("The initial cost of the asset.")),
        arg("salvage (number)", _t("The value of the asset at the end of depreciation.")),
        arg("life (number)", _t("The number of periods over which the asset is depreciated.")),
        arg("period (number)", _t("The single period within life for which to calculate depreciation.")),
    ],
    compute: function (cost, salvage, life, period) {
        const _cost = toNumber(cost, this.locale);
        const _salvage = toNumber(salvage, this.locale);
        const _life = toNumber(life, this.locale);
        const _period = toNumber(period, this.locale);
        if (_period <= 0) {
            return new EvaluationError(expectPeriodStrictlyPositive(_period));
        }
        if (_life <= 0) {
            return new EvaluationError(expectLifeStrictlyPositive(_life));
        }
        if (_period > _life) {
            return new EvaluationError(expectPeriodSmallerOrEqualToLife(_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 {
            value: (_cost - _salvage) * (remainingPeriods / deprecFactor),
            format: "#,##0.00",
        };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// TBILLPRICE
// -----------------------------------------------------------------------------
function tBillPrice(start, end, 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 = getYearFrac(start, end, 2);
    return 100 * (1 - disc * yearFrac);
}
const TBILLPRICE = {
    description: _t("Price of a US Treasury bill."),
    args: [
        arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")),
        arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")),
        arg("discount (number)", _t("The discount rate of the bill at time of purchase.")),
    ],
    compute: function (settlement, maturity, discount) {
        const start = Math.trunc(toNumber(settlement, this.locale));
        const end = Math.trunc(toNumber(maturity, this.locale));
        const disc = toNumber(discount, this.locale);
        if (start >= end) {
            return new EvaluationError(expectMaturityStrictlyGreaterThanSettlement(start, end));
        }
        if (!isSettlementLessThanOneYearBeforeMaturity(start, end, this.locale)) {
            return new EvaluationError(expectSettlementLessThanOneYearBeforeMaturity(start, end));
        }
        if (disc <= 0) {
            return new EvaluationError(expectDiscountStrictlyPositive(disc));
        }
        if (disc >= 1) {
            return new EvaluationError(expectDiscountStrictlySmallerThanOne(disc));
        }
        return tBillPrice(start, end, disc);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// TBILLEQ
// -----------------------------------------------------------------------------
const TBILLEQ = {
    description: _t("Equivalent rate of return for a US Treasury bill."),
    args: [
        arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")),
        arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")),
        arg("discount (number)", _t("The discount rate of the bill at time of purchase.")),
    ],
    compute: function (settlement, maturity, discount) {
        const start = Math.trunc(toNumber(settlement, this.locale));
        const end = Math.trunc(toNumber(maturity, this.locale));
        const disc = toNumber(discount, this.locale);
        if (start >= end) {
            return new EvaluationError(expectMaturityStrictlyGreaterThanSettlement(start, end));
        }
        if (!isSettlementLessThanOneYearBeforeMaturity(start, end, this.locale)) {
            return new EvaluationError(expectSettlementLessThanOneYearBeforeMaturity(start, end));
        }
        if (disc <= 0) {
            return new EvaluationError(expectDiscountStrictlyPositive(disc));
        }
        if (disc >= 1) {
            return new EvaluationError(expectDiscountStrictlySmallerThanOne(disc));
        }
        /**
         * https://support.microsoft.com/en-us/office/tbilleq-function-2ab72d90-9b4d-4efe-9fc2-0f81f2c19c8c
         *
         *               365 * discount
         * TBILLEQ = ________________________
         *            360 - discount * DSM
         *
         * with DSM = number of days from settlement to maturity
         *
         * What is not indicated in the Excel documentation is that this formula only works for duration between settlement
         * and maturity that are less than 6 months (182 days). This is because US Treasury bills use semi-annual interest,
         * and thus we have to take into account the compound interest for the calculation.
         *
         * For this case, the formula becomes (Treasury Securities and Derivatives, by Frank J. Fabozzi, page 49)
         *
         *            -2X + 2* SQRT[ X² - (2X - 1) * (1 - 100/p) ]
         * TBILLEQ = ________________________________________________
         *                            2X - 1
         *
         * with X = DSM / (number of days in a year),
         *  and p is the price, computed with TBILLPRICE
         *
         * Note that from my tests in Excel, we take (number of days in a year) = 366 ONLY if DSM is 366, not if
         * the settlement year is a leap year.
         *
         */
        const nDays = DAYS.compute.bind(this)({ value: end }, { value: start });
        if (nDays <= 182) {
            return (365 * disc) / (360 - disc * nDays);
        }
        const p = tBillPrice(start, end, disc) / 100;
        const daysInYear = nDays === 366 ? 366 : 365;
        const x = nDays / daysInYear;
        const num = -2 * x + 2 * Math.sqrt(x ** 2 - (2 * x - 1) * (1 - 1 / p));
        const denom = 2 * x - 1;
        return num / denom;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// TBILLYIELD
// -----------------------------------------------------------------------------
const TBILLYIELD = {
    description: _t("The yield of a US Treasury bill based on price."),
    args: [
        arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")),
        arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")),
        arg("price (number)", _t("The price at which the security is bought per 100 face value.")),
    ],
    compute: function (settlement, maturity, price) {
        const start = Math.trunc(toNumber(settlement, this.locale));
        const end = Math.trunc(toNumber(maturity, this.locale));
        const p = toNumber(price, this.locale);
        if (start >= end) {
            return new EvaluationError(expectMaturityStrictlyGreaterThanSettlement(start, end));
        }
        if (!isSettlementLessThanOneYearBeforeMaturity(start, end, this.locale)) {
            return new EvaluationError(expectSettlementLessThanOneYearBeforeMaturity(start, end));
        }
        if (p <= 0) {
            return new EvaluationError(expectPriceStrictlyPositive(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 = getYearFrac(start, end, 2);
        return ((100 - p) / p) * (1 / yearFrac);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// VDB
// -----------------------------------------------------------------------------
const DEFAULT_VDB_NO_SWITCH = false;
const VDB = {
    description: _t("Variable declining balance. WARNING : does not handle decimal periods."),
    args: [
        arg("cost (number)", _t("The initial cost of the asset.")),
        arg("salvage (number)", _t("The value of the asset at the end of depreciation.")),
        arg("life (number)", _t("The number of periods over which the asset is depreciated.")),
        arg("start (number)", _t("Starting period to calculate depreciation.")),
        arg("end (number)", _t("Ending period to calculate depreciation.")),
        arg(`factor (number, default=${DEFAULT_DDB_DEPRECIATION_FACTOR})`, _t("The number of months in the first year of depreciation.")),
        arg(`no_switch (number, default=${DEFAULT_VDB_NO_SWITCH})`, _t("Whether to switch to straight-line depreciation when the depreciation is greater than the declining balance calculation."), [
            { value: false, label: _t("Switch to straight-line depreciation") },
            { value: true, label: _t("Do not switch to straight-line depreciation") },
        ]),
    ],
    compute: function (cost, salvage, life, startPeriod, endPeriod, factor = { value: DEFAULT_DDB_DEPRECIATION_FACTOR }, noSwitch = { value: DEFAULT_VDB_NO_SWITCH }) {
        factor = factor || 0;
        const _cost = toNumber(cost, this.locale);
        const _salvage = toNumber(salvage, this.locale);
        const _life = toNumber(life, this.locale);
        /* TODO : handle decimal periods
         * on end_period it looks like it is a simple linear function, but I cannot understand exactly how
         * decimals periods are handled with start_period.
         */
        const _startPeriod = Math.trunc(toNumber(startPeriod, this.locale));
        const _endPeriod = Math.trunc(toNumber(endPeriod, this.locale));
        const _factor = toNumber(factor, this.locale);
        const _noSwitch = toBoolean(noSwitch);
        if (_cost < 0) {
            return new EvaluationError(expectCostPositiveOrZero(_cost));
        }
        if (_salvage < 0) {
            return new EvaluationError(expectSalvagePositiveOrZero(_salvage));
        }
        if (_life <= 0) {
            return new EvaluationError(expectLifeStrictlyPositive(_life));
        }
        if (_startPeriod < 0) {
            return new EvaluationError(expectStartPeriodPositiveOrZero(_startPeriod));
        }
        if (_endPeriod < 0) {
            return new EvaluationError(expectEndPeriodPositiveOrZero(_endPeriod));
        }
        if (_startPeriod > _endPeriod) {
            return new EvaluationError(expectStartPeriodSmallerOrEqualEndPeriod(_startPeriod, _endPeriod));
        }
        if (_endPeriod > _life) {
            return new EvaluationError(expectEndPeriodSmallerOrEqualToLife(_endPeriod, _life));
        }
        if (_factor <= 0) {
            return new EvaluationError(expectDeprecationFactorStrictlyPositive(_factor));
        }
        if (_cost === 0)
            return 0;
        if (_salvage >= _cost) {
            return _startPeriod < 1 ? _cost - _salvage : 0;
        }
        const doubleDeprecFactor = _factor / _life;
        if (doubleDeprecFactor >= 1) {
            return _startPeriod < 1 ? _cost - _salvage : 0;
        }
        let previousCost = _cost;
        let currentDeprec = 0;
        let resultDeprec = 0;
        let isLinearDeprec = false;
        for (let i = 0; i < _endPeriod; i++) {
            // compute the current deprecation, or keep the last one if we reached a stage of linear deprecation
            if (!isLinearDeprec || _noSwitch) {
                const doubleDeprec = previousCost * doubleDeprecFactor;
                const remainingPeriods = _life - i;
                const linearDeprec = (previousCost - _salvage) / remainingPeriods;
                if (!_noSwitch && linearDeprec > doubleDeprec) {
                    isLinearDeprec = true;
                    currentDeprec = linearDeprec;
                }
                else {
                    currentDeprec = doubleDeprec;
                }
            }
            const nextCost = Math.max(previousCost - currentDeprec, _salvage);
            if (i >= _startPeriod) {
                resultDeprec += previousCost - nextCost;
            }
            previousCost = nextCost;
        }
        return resultDeprec;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// XIRR
// -----------------------------------------------------------------------------
const XIRR = {
    description: _t("Internal rate of return given non-periodic cash flows."),
    args: [
        arg("cashflow_amounts (range<number>)", _t("An range containing the income or payments associated with the investment.")),
        arg("cashflow_dates (range<number>)", _t("An range with dates corresponding to the cash flows in cashflow_amounts.")),
        arg(`rate_guess (number, default=${RATE_GUESS_DEFAULT})`, _t("An estimate for what the internal rate of return will be.")),
    ],
    compute: function (cashflowAmounts, cashflowDates, rateGuess = { value: RATE_GUESS_DEFAULT }) {
        const guess = toNumber(rateGuess, this.locale);
        if (!areSameDimensions(cashflowAmounts, cashflowDates)) {
            return new EvaluationError(expectCashFlowsAndDatesHaveSameDimension);
        }
        const _cashFlows = cashflowAmounts.flat().map((val) => toNumber(val, this.locale));
        const _dates = cashflowDates.flat().map((val) => toNumber(val, this.locale));
        if (!havePositiveAndNegativeValues(_cashFlows)) {
            return new EvaluationError(expectCashFlowsHavePositiveAndNegativesValues);
        }
        if (_dates.some((date) => date < _dates[0])) {
            return new EvaluationError(expectEveryDateGreaterThanFirstDateOfCashFlowDates(_dates[0]));
        }
        if (guess <= -1) {
            return new EvaluationError(expectRateGuessStrictlyGreaterThanMinusOne(guess));
        }
        const map = new Map();
        for (const i of range(0, _dates.length)) {
            const date = _dates[i];
            if (map.has(date))
                map.set(date, map.get(date) + _cashFlows[i]);
            else
                map.set(date, _cashFlows[i]);
        }
        const dates = Array.from(map.keys());
        const values = dates.map((date) => map.get(date));
        /**
         * https://support.microsoft.com/en-us/office/xirr-function-de1242ec-6477-445b-b11b-a303ad9adc9d
         *
         * The rate is computed iteratively by trying to solve the equation
         *
         *
         * 0 =    SUM     [ P_i * (1 + rate) ^((d_0 - d_i) / 365) ]  + P_0
         *     i = 1 => n
         *
         * with P_i = price number i
         *      d_i = date number i
         *
         * This function is not defined for rate < -1. For the case where we get rates < -1 in the Newton method, add
         * a fallback for a number very close to -1 to continue the Newton method.
         *
         */
        const func = (rate) => {
            let value = values[0];
            for (const i of range(1, values.length)) {
                const dateDiff = (dates[0] - dates[i]) / 365;
                value += values[i] * (1 + rate) ** dateDiff;
            }
            return value;
        };
        const derivFunc = (rate) => {
            let deriv = 0;
            for (const i of range(1, values.length)) {
                const dateDiff = (dates[0] - dates[i]) / 365;
                deriv += dateDiff * values[i] * (1 + rate) ** (dateDiff - 1);
            }
            return deriv;
        };
        const nanFallback = (previousFallback) => {
            // -0.9 => -0.99 => -0.999 => ...
            if (!previousFallback)
                return -0.9;
            return previousFallback / 10 - 0.9;
        };
        return newtonMethod(func, derivFunc, guess, 40, 1e-5, nanFallback);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// XNPV
// -----------------------------------------------------------------------------
const XNPV = {
    description: _t("Net present value given to non-periodic cash flows.."),
    args: [
        arg("discount (number)", _t("The discount rate of the investment over one period.")),
        arg("cashflow_amounts (number, range<number>)", _t("An range containing the income or payments associated with the investment.")),
        arg("cashflow_dates (number, range<number>)", _t("An range with dates corresponding to the cash flows in cashflow_amounts.")),
    ],
    compute: function (discount, cashflowAmounts, cashflowDates) {
        const rate = toNumber(discount, this.locale);
        if (!areSameDimensions(cashflowAmounts, cashflowDates)) {
            return new EvaluationError(expectCashFlowsAndDatesHaveSameDimension);
        }
        const _cashFlows = toMatrix(cashflowAmounts)
            .flat()
            .map((val) => strictToNumber(val, this.locale));
        const _dates = toMatrix(cashflowDates)
            .flat()
            .map((val) => strictToNumber(val, this.locale));
        if (_dates.some((date) => date < _dates[0])) {
            return new EvaluationError(expectEveryDateGreaterThanFirstDateOfCashFlowDates(_dates[0]));
        }
        if (rate <= 0) {
            return new EvaluationError(expectRateStrictlyPositive(rate));
        }
        if (_cashFlows.length === 1)
            return _cashFlows[0];
        // aggregate values of the same date
        const map = new Map();
        for (const i of range(0, _dates.length)) {
            const date = _dates[i];
            if (map.has(date))
                map.set(date, map.get(date) + _cashFlows[i]);
            else
                map.set(date, _cashFlows[i]);
        }
        const dates = Array.from(map.keys());
        const values = dates.map((date) => map.get(date));
        /**
         * https://support.microsoft.com/en-us/office/xirr-function-de1242ec-6477-445b-b11b-a303ad9adc9d
         *
         * The present value is computed using
         *
         *
         * NPV =    SUM     [ P_i *(1 + rate) ^((d_0 - d_i) / 365) ]  + P_0
         *       i = 1 => n
         *
         * with P_i = price number i
         *      d_i = date number i
         *
         *
         */
        let pv = values[0];
        for (const i of range(1, values.length)) {
            const dateDiff = (dates[0] - dates[i]) / 365;
            pv += values[i] * (1 + rate) ** dateDiff;
        }
        return pv;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// YIELD
// -----------------------------------------------------------------------------
const YIELD = {
    description: _t("Annual yield of a security paying periodic interest."),
    args: [
        arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")),
        arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")),
        arg("rate (number)", _t("The annualized rate of interest.")),
        arg("price (number)", _t("The price at which the security is bought per 100 face value.")),
        arg("redemption (number)", _t("The redemption amount per 100 face value, or par.")),
        arg("frequency (number)", _t("The number of interest or coupon payments per year (1, 2, or 4)."), FREQUENCY_OPTIONS),
        arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use."), DAY_COUNT_CONVENTION_OPTIONS),
    ],
    compute: function (settlement, maturity, rate, price, redemption, frequency, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {
        dayCountConvention = dayCountConvention || 0;
        const _settlement = Math.trunc(toNumber(settlement, this.locale));
        const _maturity = Math.trunc(toNumber(maturity, this.locale));
        const _rate = toNumber(rate, this.locale);
        const _price = toNumber(price, this.locale);
        const _redemption = toNumber(redemption, this.locale);
        const _frequency = Math.trunc(toNumber(frequency, this.locale));
        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
        if (_settlement >= _maturity) {
            return new EvaluationError(expectMaturityStrictlyGreaterThanSettlement(_settlement, _maturity));
        }
        if (isInvalidFrequency(_frequency)) {
            return new EvaluationError(expectCouponFrequencyIsValid(_frequency));
        }
        if (isInvalidDayCountConvention(_dayCountConvention)) {
            return new EvaluationError(expectDayCountConventionIsValid(_dayCountConvention));
        }
        if (_rate < 0) {
            return new EvaluationError(expectRatePositiveOrZero(_rate));
        }
        if (_price <= 0) {
            return new EvaluationError(expectPriceStrictlyPositive(_price));
        }
        if (_redemption <= 0) {
            return new EvaluationError(expectRedemptionStrictlyPositive(_redemption));
        }
        const years = getYearFrac(_settlement, _maturity, _dayCountConvention);
        const nbrRealCoupons = years * _frequency;
        const nbrFullCoupons = Math.ceil(nbrRealCoupons);
        const timeFirstCoupon = nbrRealCoupons - Math.floor(nbrRealCoupons) || 1;
        const cashFlowFromCoupon = (100 * _rate) / _frequency;
        if (nbrFullCoupons === 1) {
            const subPart = _price + cashFlowFromCoupon * (1 - timeFirstCoupon);
            return (((_redemption + cashFlowFromCoupon - subPart) * _frequency * (1 / timeFirstCoupon)) /
                subPart);
        }
        // The result of YIELD function is the yield at which the PRICE function will return the given price.
        // This algorithm uses the Newton's method on the PRICE function to determine the result.
        // Newton's method: https://en.wikipedia.org/wiki/Newton%27s_method
        // As the PRICE function isn't continuous, we apply the Newton's method on the numerator of the PRICE formula.
        // For simplicity, it is not yield but yieldFactorPerPeriod (= 1 + yield / frequency) which will be calibrated in Newton's method.
        // yield can be deduced from yieldFactorPerPeriod in sequence.
        function priceNumerator(price, timeFirstCoupon, nbrFullCoupons, yieldFactorPerPeriod, cashFlowFromCoupon, redemption) {
            let result = redemption -
                (price + cashFlowFromCoupon * (1 - timeFirstCoupon)) *
                    yieldFactorPerPeriod ** (nbrFullCoupons - 1 + timeFirstCoupon);
            for (let i = 1; i <= nbrFullCoupons; i++) {
                result += cashFlowFromCoupon * yieldFactorPerPeriod ** (i - 1);
            }
            return result;
        }
        function priceNumeratorDeriv(price, timeFirstCoupon, nbrFullCoupons, yieldFactorPerPeriod, cashFlowFromCoupon) {
            let result = -(price + cashFlowFromCoupon * (1 - timeFirstCoupon)) *
                (nbrFullCoupons - 1 + timeFirstCoupon) *
                yieldFactorPerPeriod ** (nbrFullCoupons - 2 + timeFirstCoupon);
            for (let i = 1; i <= nbrFullCoupons; i++) {
                result += cashFlowFromCoupon * (i - 1) * yieldFactorPerPeriod ** (i - 2);
            }
            return result;
        }
        function func(x) {
            return priceNumerator(_price, timeFirstCoupon, nbrFullCoupons, x, cashFlowFromCoupon, _redemption);
        }
        function derivFunc(x) {
            return priceNumeratorDeriv(_price, timeFirstCoupon, nbrFullCoupons, x, cashFlowFromCoupon);
        }
        const initYield = _rate + 1;
        const initYieldFactorPerPeriod = 1 + initYield / _frequency;
        const methodResult = newtonMethod(func, derivFunc, initYieldFactorPerPeriod, 100, 1e-5);
        return (methodResult - 1) * _frequency;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// YIELDDISC
// -----------------------------------------------------------------------------
const YIELDDISC = {
    description: _t("Annual yield of a discount security."),
    args: [
        arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")),
        arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")),
        arg("price (number)", _t("The price at which the security is bought per 100 face value.")),
        arg("redemption (number)", _t("The redemption amount per 100 face value, or par.")),
        arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use."), DAY_COUNT_CONVENTION_OPTIONS),
    ],
    compute: function (settlement, maturity, price, redemption, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {
        dayCountConvention = dayCountConvention || 0;
        const _settlement = Math.trunc(toNumber(settlement, this.locale));
        const _maturity = Math.trunc(toNumber(maturity, this.locale));
        const _price = toNumber(price, this.locale);
        const _redemption = toNumber(redemption, this.locale);
        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
        if (_settlement >= _maturity) {
            return new EvaluationError(expectMaturityStrictlyGreaterThanSettlement(_settlement, _maturity));
        }
        if (isInvalidDayCountConvention(_dayCountConvention)) {
            return new EvaluationError(expectDayCountConventionIsValid(_dayCountConvention));
        }
        if (_price <= 0) {
            return new EvaluationError(expectPriceStrictlyPositive(_price));
        }
        if (_redemption <= 0) {
            return new EvaluationError(expectRedemptionStrictlyPositive(_redemption));
        }
        /**
         * https://wiki.documentfoundation.org/Documentation/Calc_Functions/YIELDDISC
         *
         *                    (redemption / price) - 1
         * YIELDDISC = _____________________________________
         *             YEARFRAC(settlement, maturity, basis)
         */
        const yearFrac = getYearFrac(_settlement, _maturity, _dayCountConvention);
        return (_redemption / _price - 1) / yearFrac;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// YIELDMAT
// -----------------------------------------------------------------------------
const YIELDMAT = {
    description: _t("Annual yield of a security paying interest at maturity."),
    args: [
        arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")),
        arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")),
        arg("issue (date)", _t("The date the security was initially issued.")),
        arg("rate (number)", _t("The annualized rate of interest.")),
        arg("price (number)", _t("The price at which the security is bought.")),
        arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use."), DAY_COUNT_CONVENTION_OPTIONS),
    ],
    compute: function (settlement, maturity, issue, rate, price, dayCountConvention = { value: DEFAULT_DAY_COUNT_CONVENTION }) {
        dayCountConvention = dayCountConvention || 0;
        const _settlement = Math.trunc(toNumber(settlement, this.locale));
        const _maturity = Math.trunc(toNumber(maturity, this.locale));
        const _issue = Math.trunc(toNumber(issue, this.locale));
        const _rate = toNumber(rate, this.locale);
        const _price = toNumber(price, this.locale);
        const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
        if (_settlement >= _maturity) {
            return new EvaluationError(expectMaturityStrictlyGreaterThanSettlement(_settlement, _maturity));
        }
        if (isInvalidDayCountConvention(_dayCountConvention)) {
            return new EvaluationError(expectDayCountConventionIsValid(_dayCountConvention));
        }
        if (_settlement < _issue) {
            return new EvaluationError(expectSettlementGreaterOrEqualToIssue(_settlement, _issue));
        }
        if (_rate < 0) {
            return new EvaluationError(expectRatePositiveOrZero(_rate));
        }
        if (_price <= 0) {
            return new EvaluationError(expectPriceStrictlyPositive(_price));
        }
        const issueToMaturity = getYearFrac(_issue, _maturity, _dayCountConvention);
        const issueToSettlement = getYearFrac(_issue, _settlement, _dayCountConvention);
        const settlementToMaturity = getYearFrac(_settlement, _maturity, _dayCountConvention);
        const numerator = (100 * (1 + _rate * issueToMaturity)) / (_price + 100 * _rate * issueToSettlement) - 1;
        return numerator / settlementToMaturity;
    },
    isExported: true,
};

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

var State;
(function (State) {
    /**
     * Initial state.
     * Expecting any reference for the left part of a range
     * e.g. "A1", "1", "A", "Sheet1!A1", "Sheet1!A"
     */
    State[State["LeftRef"] = 0] = "LeftRef";
    /**
     * Expecting any reference for the right part of a range
     * e.g. "A1", "1", "A", "Sheet1!A1", "Sheet1!A"
     */
    State[State["RightRef"] = 1] = "RightRef";
    /**
     * Expecting the separator without any constraint on the right part
     */
    State[State["Separator"] = 2] = "Separator";
    /**
     * Expecting the separator for a full column range
     */
    State[State["FullColumnSeparator"] = 3] = "FullColumnSeparator";
    /**
     * Expecting the separator for a full row range
     */
    State[State["FullRowSeparator"] = 4] = "FullRowSeparator";
    /**
     * Expecting the right part of a full column range
     * e.g. "1", "A1"
     */
    State[State["RightColumnRef"] = 5] = "RightColumnRef";
    /**
     * Expecting the right part of a full row range
     * e.g. "A", "A1"
     */
    State[State["RightRowRef"] = 6] = "RightRowRef";
    /**
     * Final state. A range has been matched
     */
    State[State["Found"] = 7] = "Found";
})(State || (State = {}));
const goTo = (state, guard = () => true) => [
    {
        goTo: state,
        guard,
    },
];
const goToMulti = (state, guard = () => true) => ({
    goTo: state,
    guard,
});
const machine = {
    [State.LeftRef]: {
        REFERENCE: goTo(State.Separator),
        NUMBER: goTo(State.FullRowSeparator),
        SYMBOL: [
            goToMulti(State.FullColumnSeparator, (token) => isColReference(token.value)),
            goToMulti(State.FullRowSeparator, (token) => isRowReference(token.value)),
        ],
    },
    [State.FullColumnSeparator]: {
        SPACE: goTo(State.FullColumnSeparator),
        OPERATOR: goTo(State.RightColumnRef, (token) => token.value === ":"),
    },
    [State.FullRowSeparator]: {
        SPACE: goTo(State.FullRowSeparator),
        OPERATOR: goTo(State.RightRowRef, (token) => token.value === ":"),
    },
    [State.Separator]: {
        SPACE: goTo(State.Separator),
        OPERATOR: goTo(State.RightRef, (token) => token.value === ":"),
    },
    [State.RightRef]: {
        SPACE: goTo(State.RightRef),
        NUMBER: goTo(State.Found),
        REFERENCE: goTo(State.Found, (token) => isSingleCellReference(token.value)),
        SYMBOL: goTo(State.Found, (token) => isColHeader(token.value) || isRowHeader(token.value)),
    },
    [State.RightColumnRef]: {
        SPACE: goTo(State.RightColumnRef),
        SYMBOL: goTo(State.Found, (token) => isColHeader(token.value)),
        REFERENCE: goTo(State.Found, (token) => isSingleCellReference(token.value)),
    },
    [State.RightRowRef]: {
        SPACE: goTo(State.RightRowRef),
        NUMBER: goTo(State.Found),
        REFERENCE: goTo(State.Found, (token) => isSingleCellReference(token.value)),
        SYMBOL: goTo(State.Found, (token) => isRowHeader(token.value)),
    },
    [State.Found]: {},
};
/**
 * Check if the list of tokens starts with a sequence of tokens representing
 * a range.
 * If a range is found, the sequence is removed from the list and is returned
 * as a single token.
 */
function matchReference(tokens) {
    let head = 0;
    let transitions = machine[State.LeftRef];
    let matchedTokens = "";
    while (transitions !== undefined) {
        const token = tokens[head++];
        if (!token) {
            return null;
        }
        const transition = transitions[token.type]?.find((transition) => transition.guard(token));
        const nextState = transition ? transition.goTo : undefined;
        switch (nextState) {
            case undefined:
                return null;
            case State.Found:
                matchedTokens += 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, locale = DEFAULT_LOCALE) {
    const tokens = tokenize(formula, locale);
    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 = ["%"];
class TokenList {
    tokens;
    currentIndex = 0;
    current;
    length;
    constructor(tokens) {
        this.tokens = tokens;
        this.current = tokens[0];
        this.length = tokens.length;
    }
    shift() {
        const current = this.tokens[this.currentIndex];
        this.current = this.tokens[++this.currentIndex];
        return current;
    }
    get next() {
        return this.tokens[this.currentIndex + 1];
    }
}
const OP_PRIORITY = {
    "%": 40,
    "^": 30,
    "*": 20,
    "/": 20,
    "+": 15,
    "-": 15,
    "&": 13,
    ">": 10,
    "<>": 10,
    ">=": 10,
    "<": 10,
    "<=": 10,
    "=": 10,
};
/**
 * Parse the next operand in an arithmetic expression.
 * e.g.
 *  for 1+2*3, the next operand is 1
 *  for (1+2)*3, the next operand is (1+2)
 *  for SUM(1,2)+3, the next operand is SUM(1,2)
 */
function parseOperand(tokens) {
    const current = tokens.shift();
    if (!current) {
        throw new BadExpressionError();
    }
    switch (current.type) {
        case "DEBUGGER":
            const next = parseExpression(tokens, 1000);
            next.debug = true;
            return next;
        case "NUMBER":
            return {
                type: "NUMBER",
                value: parseNumber(current.value, DEFAULT_LOCALE),
                tokenStartIndex: current.tokenIndex,
                tokenEndIndex: current.tokenIndex,
            };
        case "STRING":
            return {
                type: "STRING",
                value: removeStringQuotes(current.value),
                tokenStartIndex: current.tokenIndex,
                tokenEndIndex: current.tokenIndex,
            };
        case "INVALID_REFERENCE":
            return {
                type: "REFERENCE",
                value: CellErrorType.InvalidReference,
                tokenStartIndex: current.tokenIndex,
                tokenEndIndex: current.tokenIndex,
            };
        case "REFERENCE":
            if (tokens.current?.value === ":" && tokens.next?.type === "REFERENCE") {
                tokens.shift();
                const rightReference = tokens.shift();
                return {
                    type: "REFERENCE",
                    value: `${current.value}:${rightReference?.value}`,
                    tokenStartIndex: current.tokenIndex,
                    tokenEndIndex: rightReference.tokenIndex,
                };
            }
            return {
                type: "REFERENCE",
                value: current.value,
                tokenStartIndex: current.tokenIndex,
                tokenEndIndex: current.tokenIndex,
            };
        case "SYMBOL":
            const value = current.value;
            const nextToken = tokens.current;
            if (nextToken?.type === "LEFT_PAREN" &&
                functionRegex.test(current.value) &&
                value === unquote(value, "'")) {
                const { args, rightParen } = parseFunctionArgs(tokens);
                return {
                    type: "FUNCALL",
                    value: value,
                    args,
                    tokenStartIndex: current.tokenIndex,
                    tokenEndIndex: rightParen.tokenIndex,
                };
            }
            const upperCaseValue = value.toUpperCase();
            if (upperCaseValue === "TRUE" || upperCaseValue === "FALSE") {
                return {
                    type: "BOOLEAN",
                    value: upperCaseValue === "TRUE",
                    tokenStartIndex: current.tokenIndex,
                    tokenEndIndex: current.tokenIndex,
                };
            }
            return {
                type: "SYMBOL",
                value: unquote(current.value, "'"),
                tokenStartIndex: current.tokenIndex,
                tokenEndIndex: current.tokenIndex,
            };
        case "LEFT_PAREN":
            const result = parseExpression(tokens);
            const rightParen = consumeOrThrow(tokens, "RIGHT_PAREN", _t("Missing closing parenthesis"));
            return {
                ...result,
                tokenStartIndex: current.tokenIndex,
                tokenEndIndex: rightParen.tokenIndex,
            };
        case "OPERATOR":
            const operator = current.value;
            if (UNARY_OPERATORS_PREFIX.includes(operator)) {
                const operand = parseExpression(tokens, OP_PRIORITY[operator]);
                return {
                    type: "UNARY_OPERATION",
                    value: operator,
                    operand,
                    tokenStartIndex: current.tokenIndex,
                    tokenEndIndex: operand.tokenEndIndex,
                };
            }
            throw new BadExpressionError(_t("Unexpected token: %s", current.value));
        default:
            throw new BadExpressionError(_t("Unexpected token: %s", current.value));
    }
}
function parseFunctionArgs(tokens) {
    consumeOrThrow(tokens, "LEFT_PAREN", _t("Missing opening parenthesis"));
    const nextToken = tokens.current;
    if (nextToken?.type === "RIGHT_PAREN") {
        const rightParen = consumeOrThrow(tokens, "RIGHT_PAREN");
        return { args: [], rightParen };
    }
    const args = [];
    args.push(parseOneFunctionArg(tokens));
    while (tokens.current?.type !== "RIGHT_PAREN") {
        consumeOrThrow(tokens, "ARG_SEPARATOR", _t("Wrong function call"));
        args.push(parseOneFunctionArg(tokens));
    }
    const rightParen = consumeOrThrow(tokens, "RIGHT_PAREN");
    return { args, rightParen };
}
function parseOneFunctionArg(tokens) {
    const nextToken = tokens.current;
    if (nextToken?.type === "ARG_SEPARATOR" || nextToken?.type === "RIGHT_PAREN") {
        // arg is empty: "sum(1,,2)" "sum(,1)" "sum(1,)"
        return {
            type: "EMPTY",
            value: "",
            tokenStartIndex: nextToken.tokenIndex,
            tokenEndIndex: nextToken.tokenIndex,
        };
    }
    return parseExpression(tokens);
}
function consumeOrThrow(tokens, type, message) {
    const token = tokens.shift();
    if (!token || token.type !== type) {
        throw new BadExpressionError(message);
    }
    return token;
}
function parseExpression(tokens, parent_priority = 0) {
    if (tokens.length === 0) {
        throw new BadExpressionError();
    }
    let left = parseOperand(tokens);
    // as long as we have operators with higher priority than the parent one,
    // continue parsing the expression because it is a child sub-expression
    while (tokens.current?.type === "OPERATOR" &&
        OP_PRIORITY[tokens.current.value] > parent_priority) {
        const operatorToken = tokens.shift();
        const operator = operatorToken.value;
        if (UNARY_OPERATORS_POSTFIX.includes(operator)) {
            left = {
                type: "UNARY_OPERATION",
                value: operator,
                operand: left,
                postfix: true,
                tokenStartIndex: left.tokenStartIndex,
                tokenEndIndex: operatorToken.tokenIndex,
            };
        }
        else {
            const right = parseExpression(tokens, OP_PRIORITY[operator]);
            left = {
                type: "BIN_OPERATION",
                value: operator,
                left,
                right,
                tokenStartIndex: left.tokenStartIndex,
                tokenEndIndex: right.tokenEndIndex,
            };
        }
    }
    return left;
}
/**
 * Parse an expression (as a string) into an AST.
 */
function parse(str) {
    return parseTokens(rangeTokenize(str));
}
function parseTokens(tokens) {
    const richTokens = tokens.map((token, index) => ({
        type: token.type,
        value: token.value,
        tokenIndex: index,
    }));
    const tokensToParse = richTokens.filter((x) => x.type !== "SPACE");
    const tokenList = new TokenList(tokensToParse);
    if (tokenList.current?.value === "=") {
        tokenList.shift();
    }
    const result = parseExpression(tokenList);
    if (tokenList.current) {
        throw new BadExpressionError();
    }
    return result;
}
/**
 * Allows to visit all nodes of an AST and apply a mapping function
 * to nodes of a specific type.
 * Useful if you want to convert some part of a formula.
 *
 * @example
 * convertAstNodes(ast, "FUNCALL", convertFormulaToExcel)
 *
 * function convertFormulaToExcel(ast: ASTFuncall) {
 *   // ...
 *   return modifiedAst
 * }
 */
function convertAstNodes(ast, type, fn) {
    return mapAst(ast, (ast) => {
        if (ast.type === type) {
            return fn(ast);
        }
        return ast;
    });
}
function iterateAstNodes(ast) {
    return Array.from(astIterator(ast));
}
function* astIterator(ast) {
    yield ast;
    switch (ast.type) {
        case "FUNCALL":
            for (const arg of ast.args) {
                yield* astIterator(arg);
            }
            break;
        case "UNARY_OPERATION":
            yield* astIterator(ast.operand);
            break;
        case "BIN_OPERATION":
            yield* astIterator(ast.left);
            yield* astIterator(ast.right);
            break;
    }
}
function mapAst(ast, fn) {
    ast = fn(ast);
    switch (ast.type) {
        case "FUNCALL":
            return {
                ...ast,
                args: ast.args.map((child) => mapAst(child, fn)),
            };
        case "UNARY_OPERATION":
            return {
                ...ast,
                operand: mapAst(ast.operand, fn),
            };
        case "BIN_OPERATION":
            return {
                ...ast,
                right: mapAst(ast.right, fn),
                left: mapAst(ast.left, fn),
            };
        default:
            return ast;
    }
}

/**
 * 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 a code representing the position of the token in an opening parentheses tree.""
 *
 * For `=SIN(0) + SUM(COS(1), ABS(-1*(2+4)))`:
 * |Tokens   | code             |
 * |---------|------------------|
 * |`=`      | undefined        |
 * |`SIN(0)` | 1                |
 * |`+`      | undefined        |
 * |`SUM(`   | 2                |
 * |`COS(1)` | 2:1              |
 * |`,`      | 2                |
 * |`ABS(-1*`| 2:2              |
 * |`(2+4)`  | 2:2:1            |
 * |`)`      | 2:2              |
 * |`)`      | 2                |
 */
function mapParenthesisCode(tokens) {
    const code = [];
    let nextLevel = 0;
    return tokens.map((token, i) => {
        if (token.type === "LEFT_PAREN") {
            code.push(nextLevel + 1);
            nextLevel = 0;
            // allows to link the parentheses opening a function to the function
            for (let previousIndex = i - 1; previousIndex >= 0; previousIndex--) {
                if (tokens[previousIndex].type === "SPACE") {
                    tokens[previousIndex].parenthesesCode = code.join(":");
                    continue;
                }
                if (tokens[previousIndex].type === "SYMBOL") {
                    tokens[previousIndex].parenthesesCode = code.join(":");
                }
                break;
            }
        }
        token.parenthesesCode = code.join(":");
        if (token.type === "RIGHT_PAREN" && code.length) {
            nextLevel = code.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) {
    const stack = [];
    let functionStarted = "";
    function pushTokenToFunctionContext(token) {
        if (stack.length === 0) {
            return;
        }
        const functionContext = stack.at(-1);
        if (functionContext && functionContext.argsTokens) {
            const { argsTokens, argPosition } = functionContext;
            if (!argsTokens[argPosition]) {
                argsTokens[argPosition] = [];
            }
            argsTokens[argPosition].push({ value: token.value, type: token.type });
        }
    }
    return tokens.map((token, i) => {
        if (!["SPACE", "LEFT_PAREN"].includes(token.type)) {
            functionStarted = "";
        }
        switch (token.type) {
            case "SYMBOL":
                pushTokenToFunctionContext(token);
                functionStarted = token.value;
                break;
            case "LEFT_PAREN":
                stack.push({ parent: functionStarted, argPosition: 0, argsTokens: [], args: [] });
                pushTokenToFunctionContext(token);
                functionStarted = "";
                break;
            case "RIGHT_PAREN":
                const child = stack.pop();
                child?.argsTokens?.flat().forEach(pushTokenToFunctionContext);
                pushTokenToFunctionContext(token);
                break;
            case "ARG_SEPARATOR":
                if (stack.length) {
                    // increment position on current function
                    stack[stack.length - 1].argPosition++;
                }
                pushTokenToFunctionContext(token);
                break;
            default:
                pushTokenToFunctionContext(token);
                break;
        }
        if (stack.length) {
            const functionContext = stack[stack.length - 1];
            if (functionContext.parent) {
                token.functionContext = Object.assign({}, functionContext);
            }
        }
        return token;
    });
}
/**
 * Parse the list of tokens that compose the arguments of a function to
 * their AST representation.
 */
function addArgsAST(tokens) {
    for (const token of tokens) {
        if (token.functionContext) {
            const { argsTokens, args } = token.functionContext;
            // remove argsTokens from the context to remove noise
            // The business logic should not need it, it is only used temporarily
            // to build the arguments ASTs.
            delete token.functionContext.argsTokens;
            if (args.length || !argsTokens) {
                // function context already process at a previous token
                continue;
            }
            if (argsTokens[0]?.[0]?.type === "LEFT_PAREN") {
                // remove the parenthesis leading the first argument
                argsTokens[0] = argsTokens[0].slice(1);
            }
            for (const argTokens of argsTokens) {
                let tokens = argTokens;
                if (tokens.at(0)?.type === "ARG_SEPARATOR") {
                    tokens = tokens.slice(1);
                }
                try {
                    args.push(parseTokens(tokens));
                }
                catch (error) {
                    args.push(undefined);
                }
            }
        }
    }
    return tokens;
}
/**
 * Take the result of the tokenizer and transform it to be usable in the composer.
 *
 * @param formula
 */
function composerTokenize(formula, locale) {
    const tokens = rangeTokenize(formula, locale);
    return addArgsAST(mapParentFunction(mapParenthesisCode(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 updatedLeft = getTokenNextReferenceType(left);
    const updatedRight = right ? `:${getTokenNextReferenceType(right)}` : "";
    return { ...token, value: getFullReference(sheetName, 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 = setXcToFixedReferenceType(xc, "colrow");
            break;
        case "colrow":
            xc = setXcToFixedReferenceType(xc, "row");
            break;
        case "row":
            xc = setXcToFixedReferenceType(xc, "col");
            break;
        case "col":
            xc = setXcToFixedReferenceType(xc, "none");
            break;
    }
    return xc;
}
/**
 * Returns the given XC with the given reference type.
 */
function setXcToFixedReferenceType(xc, referenceType) {
    let sheetName;
    ({ sheetName, xc } = splitReference(xc));
    sheetName = sheetName ? getCanonicalSymbolName(sheetName) + "!" : "";
    xc = xc.replace(/\$/g, "");
    const splitIndex = xc.indexOf(":");
    if (splitIndex >= 0) {
        return `${sheetName}${_setXcToFixedReferenceType(xc.slice(0, splitIndex), referenceType)}:${_setXcToFixedReferenceType(xc.slice(splitIndex + 1), referenceType)}`;
    }
    else {
        return sheetName + _setXcToFixedReferenceType(xc, referenceType);
    }
}
function _setXcToFixedReferenceType(xc, referenceType) {
    const indexOfNumber = xc.search(/[0-9]/);
    const hasCol = indexOfNumber !== 0;
    const hasRow = indexOfNumber >= 0;
    switch (referenceType) {
        case "col":
            if (!hasCol)
                return xc;
            return "$" + xc;
        case "row":
            if (!hasRow)
                return xc;
            return xc.slice(0, indexOfNumber) + "$" + xc.slice(indexOfNumber);
        case "colrow":
            if (!hasRow || !hasCol)
                return "$" + xc;
            return "$" + xc.slice(0, indexOfNumber) + "$" + xc.slice(indexOfNumber);
        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("$");
}
/**
 * Return the cycled reference if any (A1 -> $A$1 -> A$1 -> $A1 -> A1)
 */
function cycleFixedReference(selection, content, locale) {
    const currentTokens = content.startsWith("=")
        ? composerTokenize(content, locale)
        : [];
    const tokens = currentTokens.filter((t) => (t.start <= selection.start && t.end >= selection.start) ||
        (t.start >= selection.start && t.start < selection.end));
    const refTokens = tokens.filter((token) => token.type === "REFERENCE");
    if (refTokens.length === 0) {
        return;
    }
    const updatedReferences = tokens
        .map(loopThroughReferenceType)
        .map((token) => token.value)
        .join("");
    const start = tokens[0].start;
    const end = tokens[tokens.length - 1].end;
    const newContent = content.slice(0, start) + updatedReferences + content.slice(end);
    const lengthDiff = newContent.length - content.length;
    const startOfTokens = refTokens[0].start;
    const endOfTokens = refTokens[refTokens.length - 1].end + lengthDiff;
    const newSelection = { start: startOfTokens, end: endOfTokens };
    if (refTokens.length === 1 && selection.start === selection.end) {
        newSelection.start = newSelection.end;
    }
    return { content: newContent, selection: newSelection };
}

// -----------------------------------------------------------------------------
// CELL
// -----------------------------------------------------------------------------
// NOTE: missing from Excel: "color", "filename", "parentheses", "prefix", "protect" and "width"
const CELL_INFO_TYPES = [
    {
        value: "address",
        label: _t("Returns an absolute reference as plain text of the top left cell in reference."),
    },
    { value: "col", label: _t("Returns the column number of the cell in reference.") },
    {
        value: "contents",
        label: _t("Returns the value contained in the top left cell in reference."),
    },
    { value: "format", label: _t("Returns the format of the top left cell in reference.") },
    { value: "row", label: _t("Returns the row number of the top left cell in reference.") },
    {
        value: "type",
        label: _t('Returns the type of data in the cell in reference. The following values are returned: "b" for a blank cell, "l" (for label) if the cell contains plain text, and "v" (for value) if the cell contains any other type of data.'),
    },
];
const CELL = {
    description: _t("Gets information about a cell."),
    args: [
        arg("info_type (string)", _t("The type of information requested."), CELL_INFO_TYPES),
        arg("reference (meta, range<meta>)", _t("The reference to the cell.")),
    ],
    compute: function (info, reference) {
        const _info = toString(info).toLowerCase();
        if (!CELL_INFO_TYPES.map((type) => type.value).includes(_info)) {
            return new EvaluationError(_t("The info_type should be one of %s.", CELL_INFO_TYPES.join(", ")));
        }
        const sheetId = this.__originSheetId;
        const _reference = reference[0][0].value;
        let { sheetName, xc } = splitReference(_reference);
        // only put the sheet name if the referenced range is in another sheet than the cell the formula is on
        sheetName = sheetName === this.getters.getSheetName(sheetId) ? undefined : sheetName;
        const fixedRef = getFullReference(sheetName, setXcToFixedReferenceType(xc, "colrow"));
        const range = this.getters.getRangeFromSheetXC(sheetId, fixedRef);
        switch (_info) {
            case "address":
                return this.getters.getRangeString(range, sheetId);
            case "col":
                return range.zone.left + 1;
            case "contents": {
                const position = { sheetId: range.sheetId, col: range.zone.left, row: range.zone.top };
                return this.getters.getEvaluatedCell(position).value;
            }
            case "format": {
                const position = { sheetId: range.sheetId, col: range.zone.left, row: range.zone.top };
                return this.getters.getEvaluatedCell(position).format || "";
            }
            case "row":
                return range.zone.top + 1;
            case "type": {
                const position = { sheetId: range.sheetId, col: range.zone.left, row: range.zone.top };
                const type = this.getters.getEvaluatedCell(position).type;
                if (type === CellValueType.empty) {
                    return "b"; // blank
                }
                else if (type === CellValueType.text) {
                    return "l"; // label
                }
                else {
                    return "v"; // value
                }
            }
        }
        return "";
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// ISERR
// -----------------------------------------------------------------------------
const ISERR = {
    description: _t("Whether a value is an error other than #N/A."),
    args: [arg("value (any)", _t("The value to be verified as an error type."))],
    compute: function (data) {
        const value = data?.value;
        return isEvaluationError(value) && value !== CellErrorType.NotAvailable;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// ISERROR
// -----------------------------------------------------------------------------
const ISERROR = {
    description: _t("Whether a value is an error."),
    args: [arg("value (any)", _t("The value to be verified as an error type."))],
    compute: function (data) {
        const value = data?.value;
        return isEvaluationError(value);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// ISLOGICAL
// -----------------------------------------------------------------------------
const ISLOGICAL = {
    description: _t("Whether a value is `true` or `false`."),
    args: [arg("value (any)", _t("The value to be verified as a logical TRUE or FALSE."))],
    compute: function (value) {
        return typeof value?.value === "boolean";
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// ISNA
// -----------------------------------------------------------------------------
const ISNA = {
    description: _t("Whether a value is the error #N/A."),
    args: [arg("value (any)", _t("The value to be verified as an error type."))],
    compute: function (data) {
        return data?.value === CellErrorType.NotAvailable;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// ISNONTEXT
// -----------------------------------------------------------------------------
const ISNONTEXT = {
    description: _t("Whether a value is non-textual."),
    args: [arg("value (any)", _t("The value to be checked."))],
    compute: function (value) {
        return !ISTEXT.compute.bind(this)(value);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// ISNUMBER
// -----------------------------------------------------------------------------
const ISNUMBER = {
    description: _t("Whether a value is a number."),
    args: [arg("value (any)", _t("The value to be verified as a number."))],
    compute: function (value) {
        return typeof value?.value === "number";
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// ISTEXT
// -----------------------------------------------------------------------------
const ISTEXT = {
    description: _t("Whether a value is text."),
    args: [arg("value (any)", _t("The value to be verified as text."))],
    compute: function (value) {
        return typeof value?.value === "string" && isEvaluationError(value?.value) === false;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// ISBLANK
// -----------------------------------------------------------------------------
const ISBLANK = {
    description: _t("Whether the referenced cell is empty"),
    args: [arg("value (any)", _t("Reference to the cell that will be checked for emptiness."))],
    compute: function (value) {
        return value?.value === null;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// NA
// -----------------------------------------------------------------------------
const NA = {
    description: _t("Returns the error value #N/A."),
    args: [],
    compute: function () {
        return { value: CellErrorType.NotAvailable };
    },
    isExported: true,
};

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

// -----------------------------------------------------------------------------
// AND
// -----------------------------------------------------------------------------
const AND = {
    description: _t("Logical `and` operator."),
    args: [
        arg("logical_expression1 (boolean, range<boolean>)", _t("An expression or reference to a cell containing an expression that represents some logical value, i.e. TRUE or FALSE, or an expression that can be coerced to a logical value.")),
        arg("logical_expression2 (boolean, range<boolean>, repeating)", _t("More expressions that represent logical values.")),
    ],
    compute: function (...logicalExpressions) {
        const { result, foundBoolean } = boolAnd(logicalExpressions);
        if (!foundBoolean) {
            return new EvaluationError(noValidInputErrorMessage);
        }
        return result;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// FALSE
// -----------------------------------------------------------------------------
const FALSE = {
    description: _t("Logical value `false`."),
    args: [],
    compute: function () {
        return false;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// IF
// -----------------------------------------------------------------------------
const IF = {
    description: _t("Returns value depending on logical expression."),
    args: [
        arg("logical_expression (boolean, range<boolean>)", _t("An expression or reference to a cell containing an expression that represents some logical value, i.e. TRUE or FALSE.")),
        arg("value_if_true (any, range)", _t("The value the function returns if logical_expression is TRUE.")),
        arg("value_if_false (any, range, default=FALSE)", _t("The value the function returns if logical_expression is FALSE.")),
    ],
    compute: function (logicalExpression, valueIfTrue, valueIfFalse) {
        if (isMultipleElementMatrix(logicalExpression)) {
            return applyVectorization(IF.compute, [logicalExpression, valueIfTrue, valueIfFalse]);
        }
        const result = toBoolean(toScalar(logicalExpression)) ? valueIfTrue : valueIfFalse;
        return result ?? { value: 0 };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// IFERROR
// -----------------------------------------------------------------------------
const IFERROR = {
    description: _t("Value if it is not an error, otherwise 2nd argument."),
    args: [
        arg("value (any, range)", _t("The value to return if value itself is not an error.")),
        arg(`value_if_error (any, range, default="empty")`, _t("The value the function returns if value is an error.")),
    ],
    compute: function (value, valueIfError) {
        if (isMultipleElementMatrix(value)) {
            return applyVectorization(IFERROR.compute, [value, valueIfError]);
        }
        const result = isEvaluationError(toScalar(value)?.value) ? valueIfError : value;
        return result ?? { value: 0 };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// IFNA
// -----------------------------------------------------------------------------
const IFNA = {
    description: _t("Value if it is not an #N/A error, otherwise 2nd argument."),
    args: [
        arg("value (any, range)", _t("The value to return if value itself is not #N/A an error.")),
        arg(`value_if_error (any, range, default="empty")`, _t("The value the function returns if value is an #N/A error.")),
    ],
    compute: function (value, valueIfError) {
        if (isMultipleElementMatrix(value)) {
            return applyVectorization(IFNA.compute, [value, valueIfError]);
        }
        const result = toScalar(value)?.value === CellErrorType.NotAvailable ? valueIfError : value;
        return result ?? { value: 0 };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// IFS
// -----------------------------------------------------------------------------
const IFS = {
    description: _t("Returns a value depending on multiple logical expressions."),
    args: [
        arg("condition1 (boolean, range<boolean>)", _t("The first condition to be evaluated. This can be a boolean, a number, an array, or a reference to any of those.")),
        arg("value1 (any, range)", _t("The returned value if condition1 is TRUE.")),
        arg("condition2 (boolean, any, range, repeating)", _t("Additional conditions to be evaluated if the previous ones are FALSE.")),
        arg("value2 (any, range, repeating)", _t("Additional values to be returned if their corresponding conditions are TRUE.")),
    ],
    compute: function (...values) {
        if (values.length % 2 !== 0) {
            return new EvaluationError(_t("Wrong number of arguments. Expected an even number of arguments."));
        }
        while (values.length > 0) {
            if (isMultipleElementMatrix(values[0])) {
                return applyVectorization(IFS.compute, values);
            }
            const condition = toBoolean(toScalar(values.shift()));
            const valueIfTrue = values.shift();
            if (condition) {
                return valueIfTrue ?? { value: 0 };
            }
        }
        return new EvaluationError(_t("No match."));
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// NOT
// -----------------------------------------------------------------------------
const NOT = {
    description: _t("Returns opposite of provided logical value."),
    args: [
        arg("logical_expression (boolean)", _t("An expression or reference to a cell holding an expression that represents some logical value.")),
    ],
    compute: function (logicalExpression) {
        return !toBoolean(logicalExpression);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// OR
// -----------------------------------------------------------------------------
const OR = {
    description: _t("Logical `or` operator."),
    args: [
        arg("logical_expression1 (boolean, range<boolean>)", _t("An expression or reference to a cell containing an expression that represents some logical value, i.e. TRUE or FALSE, or an expression that can be coerced to a logical value.")),
        arg("logical_expression2 (boolean, range<boolean>, repeating)", _t("More expressions that evaluate to logical values.")),
    ],
    compute: function (...logicalExpressions) {
        const { result, foundBoolean } = boolOr(logicalExpressions);
        if (!foundBoolean) {
            return new EvaluationError(noValidInputErrorMessage);
        }
        return result;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// SWITCH
// -----------------------------------------------------------------------------
const SWITCH = {
    description: _t("Returns a value by comparing cases to an expression."),
    args: [
        arg("expression (number, boolean, string)", _t("The value to be checked.")),
        arg("case1 (number, boolean, string)", _t("The first case to be checked against expression.")),
        arg("value1 (any)", _t("The corresponding value to be returned if case1 matches expression.")),
        arg("case2 (any, repeating)", _t("Additional cases to try if the previous ones don't match expression.")),
        arg("value2 (any, repeating)", _t("Additional values to be returned if their corresponding cases match expression.")),
        arg(`default (any, default="empty")`, _t("An optional default value to be returned if none of the cases match expression.")),
    ],
    compute: function (expression, ...casesAndValues) {
        const defaultValue = casesAndValues.length % 2 === 0 ? valueNotAvailable(expression) : casesAndValues.pop();
        for (let i = 0; i < casesAndValues.length; i += 2) {
            const iCase = casesAndValues[i];
            if (iCase && isEvaluationError(iCase.value)) {
                return iCase;
            }
            if (expression?.value === iCase?.value) {
                return casesAndValues[i + 1] || { value: 0 };
            }
        }
        return defaultValue || { value: 0 };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// TRUE
// -----------------------------------------------------------------------------
const TRUE = {
    description: _t("Logical value `true`."),
    args: [],
    compute: function () {
        return true;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// XOR
// -----------------------------------------------------------------------------
const XOR = {
    description: _t("Logical `xor` operator."),
    args: [
        arg("logical_expression1 (boolean, range<boolean>)", _t("An expression or reference to a cell containing an expression that represents some logical value, i.e. TRUE or FALSE, or an expression that can be coerced to a logical value.")),
        arg("logical_expression2 (boolean, range<boolean>, repeating)", _t("More expressions that evaluate to logical values.")),
    ],
    compute: function (...logicalExpressions) {
        let foundBoolean = false;
        let acc = false;
        conditionalVisitBoolean(logicalExpressions, (arg) => {
            foundBoolean = true;
            acc = acc ? !arg : arg;
            return true; // no stop condition
        });
        if (!foundBoolean) {
            return new EvaluationError(noValidInputErrorMessage);
        }
        return acc;
    },
    isExported: true,
};

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

const CfTerms = {
    Errors: {
        ["InvalidRange" /* CommandResult.InvalidRange */]: _t("The range is invalid"),
        ["FirstArgMissing" /* CommandResult.FirstArgMissing */]: _t("The argument is missing. Please provide a value"),
        ["SecondArgMissing" /* CommandResult.SecondArgMissing */]: _t("The second argument is missing. Please provide a value"),
        ["MinNaN" /* CommandResult.MinNaN */]: _t("The minpoint must be a number"),
        ["MidNaN" /* CommandResult.MidNaN */]: _t("The midpoint must be a number"),
        ["MaxNaN" /* CommandResult.MaxNaN */]: _t("The maxpoint must be a number"),
        ["ValueUpperInflectionNaN" /* CommandResult.ValueUpperInflectionNaN */]: _t("The first value must be a number"),
        ["ValueLowerInflectionNaN" /* CommandResult.ValueLowerInflectionNaN */]: _t("The second value must be a number"),
        ["MinBiggerThanMax" /* CommandResult.MinBiggerThanMax */]: _t("Minimum must be smaller then Maximum"),
        ["MinBiggerThanMid" /* CommandResult.MinBiggerThanMid */]: _t("Minimum must be smaller then Midpoint"),
        ["MidBiggerThanMax" /* CommandResult.MidBiggerThanMax */]: _t("Midpoint must be smaller then Maximum"),
        ["LowerBiggerThanUpper" /* CommandResult.LowerBiggerThanUpper */]: _t("Lower inflection point must be smaller than upper inflection point"),
        ["MinInvalidFormula" /* CommandResult.MinInvalidFormula */]: _t("Invalid Minpoint formula"),
        ["MaxInvalidFormula" /* CommandResult.MaxInvalidFormula */]: _t("Invalid Maxpoint formula"),
        ["MidInvalidFormula" /* CommandResult.MidInvalidFormula */]: _t("Invalid Midpoint formula"),
        ["ValueUpperInvalidFormula" /* CommandResult.ValueUpperInvalidFormula */]: _t("Invalid upper inflection point formula"),
        ["ValueLowerInvalidFormula" /* CommandResult.ValueLowerInvalidFormula */]: _t("Invalid lower inflection point formula"),
        ["EmptyRange" /* CommandResult.EmptyRange */]: _t("A range needs to be defined"),
        ["ValueCellIsInvalidFormula" /* CommandResult.ValueCellIsInvalidFormula */]: _t("At least one of the provided values is an invalid formula"),
        Unexpected: _t("The rule is invalid for an unknown reason"),
    },
    ColorScale: _t("Color scale"),
    IconSet: _t("Icon set"),
    DataBar: _t("Data bar"),
};
const ChartTerms = {
    Series: _t("Series"),
    BackgroundColor: _t("Background color"),
    StackedBarChart: _t("Stacked bar chart"),
    StackedLineChart: _t("Stacked line chart"),
    StackedAreaChart: _t("Stacked area chart"),
    StackedColumnChart: _t("Stacked column chart"),
    CumulativeData: _t("Cumulative data"),
    TreatLabelsAsText: _t("Treat labels as text"),
    AggregatedChart: _t("Aggregate"),
    Errors: {
        Unexpected: _t("The chart definition is invalid for an unknown reason"),
        // BASIC CHART ERRORS (LINE | BAR | PIE)
        ["InvalidDataSet" /* CommandResult.InvalidDataSet */]: _t("The dataset is invalid"),
        ["InvalidLabelRange" /* CommandResult.InvalidLabelRange */]: _t("Labels are invalid"),
        // SCORECARD CHART ERRORS
        ["InvalidScorecardKeyValue" /* CommandResult.InvalidScorecardKeyValue */]: _t("The key value is invalid"),
        ["InvalidScorecardBaseline" /* CommandResult.InvalidScorecardBaseline */]: _t("The baseline value is invalid"),
        // GAUGE CHART ERRORS
        ["InvalidGaugeDataRange" /* CommandResult.InvalidGaugeDataRange */]: _t("The data range is invalid"),
        ["EmptyGaugeRangeMin" /* CommandResult.EmptyGaugeRangeMin */]: _t("A minimum range limit value is needed"),
        ["GaugeRangeMinNaN" /* CommandResult.GaugeRangeMinNaN */]: _t("The minimum range limit value must be a number"),
        ["EmptyGaugeRangeMax" /* CommandResult.EmptyGaugeRangeMax */]: _t("A maximum range limit value is needed"),
        ["GaugeRangeMaxNaN" /* CommandResult.GaugeRangeMaxNaN */]: _t("The maximum range limit value must be a number"),
        ["GaugeLowerInflectionPointNaN" /* CommandResult.GaugeLowerInflectionPointNaN */]: _t("The lower inflection point value must be a number"),
        ["GaugeUpperInflectionPointNaN" /* CommandResult.GaugeUpperInflectionPointNaN */]: _t("The upper inflection point value must be a number"),
    },
    GeoChart: {
        ColorScales: {
            blues: _t("Blues"),
            cividis: _t("Cividis"),
            greens: _t("Greens"),
            greys: _t("Greys"),
            oranges: _t("Oranges"),
            purples: _t("Purples"),
            rainbow: _t("Rainbow"),
            reds: _t("Reds"),
            viridis: _t("Viridis"),
        },
    },
};
const CustomCurrencyTerms = {
    Custom: _t("Custom"),
};
const MergeErrorMessage = _t("Merged cells are preventing this operation. Unmerge those cells and try again.");
const TableHeaderMoveErrorMessage = _t("The header row of a table can't be moved.");
const SplitToColumnsTerms = {
    Errors: {
        Unexpected: _t("Cannot split the selection for an unknown reason"),
        ["NoSplitSeparatorInSelection" /* CommandResult.NoSplitSeparatorInSelection */]: _t("There is no match for the selected separator in the selection"),
        ["MoreThanOneColumnSelected" /* CommandResult.MoreThanOneColumnSelected */]: _t("Only a selection from a single column can be split"),
        ["SplitWillOverwriteContent" /* CommandResult.SplitWillOverwriteContent */]: _t("Splitting will overwrite existing content"),
    },
};
const RemoveDuplicateTerms = {
    Errors: {
        Unexpected: _t("Cannot remove duplicates for an unknown reason"),
        ["MoreThanOneRangeSelected" /* CommandResult.MoreThanOneRangeSelected */]: _t("Please select only one range of cells"),
        ["EmptyTarget" /* CommandResult.EmptyTarget */]: _t("Please select a range of cells containing values."),
        ["NoColumnsProvided" /* CommandResult.NoColumnsProvided */]: _t("Please select at latest one column to analyze."),
        //TODO: Remove it when accept to copy and paste merge cells
        ["WillRemoveExistingMerge" /* CommandResult.WillRemoveExistingMerge */]: _t("This operation is not possible due to a merge. Please remove the merges first than try again."),
    },
};
const DVTerms = {
    DateIs: {
        today: _t("today"),
        yesterday: _t("yesterday"),
        tomorrow: _t("tomorrow"),
        lastWeek: _t("in the past week"),
        lastMonth: _t("in the past month"),
        lastYear: _t("in the past year"),
    },
    DateIsBefore: {
        today: _t("today"),
        yesterday: _t("yesterday"),
        tomorrow: _t("tomorrow"),
        lastWeek: _t("one week ago"),
        lastMonth: _t("one month ago"),
        lastYear: _t("one year ago"),
    },
    CriterionError: {
        notEmptyValue: _t("The value must not be empty"),
        numberValue: _t("The value must be a number"),
        dateValue: _t("The value must be a date"),
        validRange: _t("The value must be a valid range"),
        validFormula: _t("The formula must be valid"),
    },
    Errors: {
        ["InvalidRange" /* CommandResult.InvalidRange */]: _t("The range is invalid."),
        ["InvalidDataValidationCriterionValue" /* CommandResult.InvalidDataValidationCriterionValue */]: _t("One or more of the provided criteria values are invalid. Please review and correct them."),
        ["InvalidNumberOfCriterionValues" /* CommandResult.InvalidNumberOfCriterionValues */]: _t("One or more of the provided criteria values are missing."),
        Unexpected: _t("The rule is invalid for an unknown reason."),
    },
};
const TableTerms = {
    Errors: {
        Unexpected: _t("The table zone is invalid for an unknown reason"),
        ["TableOverlap" /* CommandResult.TableOverlap */]: _t("You cannot create overlapping tables."),
        ["NonContinuousTargets" /* CommandResult.NonContinuousTargets */]: _t("A table can only be created on a continuous selection."),
        ["InvalidRange" /* CommandResult.InvalidRange */]: _t("The range is invalid"),
        ["TargetOutOfSheet" /* CommandResult.TargetOutOfSheet */]: _t("The range is out of the sheet"),
    },
    Checkboxes: {
        hasFilters: _t("Filter button"),
        headerRow: _t("Header row(s)"),
        bandedRows: _t("Banded rows"),
        firstColumn: _t("First column"),
        lastColumn: _t("Last column"),
        bandedColumns: _t("Banded columns"),
        automaticAutofill: _t("Automatically autofill formulas"),
        totalRow: _t("Total row"),
        isDynamic: _t("Auto-adjust to formula result"),
    },
    Tooltips: {
        filterWithoutHeader: _t("Cannot have filters without a header row"),
        isDynamic: _t("For tables based on array formulas only"),
    },
};
const measureDisplayTerms = {
    labels: {
        no_calculations: _t("No calculations"),
        "%_of_grand_total": _t("% of grand total"),
        "%_of_col_total": _t("% of column total"),
        "%_of_row_total": _t("% of row total"),
        "%_of": _t("% of"),
        "%_of_parent_row_total": _t("% of parent row total"),
        "%_of_parent_col_total": _t("% of parent column total"),
        "%_of_parent_total": _t("% of parent total"),
        difference_from: _t("Difference from"),
        "%_difference_from": _t("% difference from"),
        running_total: _t("Running total"),
        "%_running_total": _t("% Running total"),
        rank_asc: _t("Rank smallest to largest"),
        rank_desc: _t("Rank largest to smallest"),
        index: _t("Index"),
    },
    descriptions: {
        "%_of_grand_total": () => _t("Displayed as % of grand total"),
        "%_of_col_total": () => _t("Displayed as % of column total"),
        "%_of_row_total": () => _t("Displayed as % of row total"),
        "%_of": (field) => _t('Displayed as % of "%s"', field),
        "%_of_parent_row_total": (field) => _t('Displayed as % of parent row total of "%s"', field),
        "%_of_parent_col_total": () => _t("Displayed as % of parent column total"),
        "%_of_parent_total": (field) => _t('Displayed as % of parent "%s" total', field),
        difference_from: (field) => _t('Displayed as difference from "%s"', field),
        "%_difference_from": (field) => _t('Displayed as % difference from "%s"', field),
        running_total: (field) => _t('Displayed as running total based on "%s"', field),
        "%_running_total": (field) => _t('Displayed as % running total based on "%s"', field),
        rank_asc: (field) => _t('Displayed as rank from smallest to largest based on "%s"', field),
        rank_desc: (field) => _t('Displayed as rank largest to smallest based on "%s"', field),
        index: () => _t("Displayed as index"),
    },
    documentation: {
        no_calculations: _t("Displays the value that is entered in the field."),
        "%_of_grand_total": _t("Displays values as a percentage of the grand total of all the values or data points in the report."),
        "%_of_col_total": _t("Displays all the values in each column or series as a percentage of the total for the column or series."),
        "%_of_row_total": _t("Displays the value in each row or category as a percentage of the total for the row or category."),
        "%_of": _t("Displays values as a percentage of the value of the Base item in the Base field."),
        "%_of_parent_row_total": _t("Calculates values as follows:\n(value for the item) / (value for the parent item on rows)"),
        "%_of_parent_col_total": _t("Calculates values as follows:\n(value for the item) / (value for the parent item on columns)"),
        "%_of_parent_total": _t("Calculates values as follows:\n(value for the item) / (value for the parent item of the selected Base field)"),
        difference_from: _t("Displays values as the difference from the value of the Base item in the Base field."),
        "%_difference_from": _t("Displays values as the percentage difference from the value of the Base item in the Base field."),
        running_total: _t("Displays the value for successive items in the Base field as a running total."),
        "%_running_total": _t("Calculates the value as a percentage for successive items in the Base field that are displayed as a running total."),
        rank_asc: _t("Displays the rank of selected values in a specific field, listing the smallest item in the field as 1, and each larger value with a higher rank value."),
        rank_desc: _t("Displays the rank of selected values in a specific field, listing the largest item in the field as 1, and each smaller value with a higher rank value."),
        index: _t("Calculates values as follows:\n((value in cell) x (Grand Total of Grand Totals)) / ((Grand Row Total) x (Grand Column Total))"),
    },
};
function getPivotTooBigErrorMessage(numberOfCells, locale) {
    const formattedNumber = formatValue(numberOfCells, {
        format: "0,00",
        locale: locale,
    });
    return _t("Oops—this pivot table is quite large (%s cells). Try simplifying it using the side panel.", formattedNumber);
}

/**
 * Get the pivot ID from the formula pivot ID.
 */
function getPivotId(pivotFormulaId, getters) {
    const pivotId = getters.getPivotId(pivotFormulaId);
    if (!pivotId) {
        throw new EvaluationError(_t('There is no pivot with id "%s"', pivotFormulaId));
    }
    return pivotId;
}
function assertMeasureExist(pivotId, measure, getters) {
    const { measures } = getters.getPivotCoreDefinition(pivotId);
    if (!measures.find((m) => m.id === measure)) {
        const validMeasures = `(${measures.map((m) => m.id).join(", ")})`;
        throw new EvaluationError(_t("The argument %s is not a valid measure. Here are the measures: %s", measure, validMeasures));
    }
}
function assertDomainLength(domain) {
    if (domain.length % 2 !== 0) {
        throw new EvaluationError(_t("Function PIVOT takes an even number of arguments."));
    }
}
function addPivotDependencies(evalContext, pivotId, forMeasures) {
    //TODO This function can be very costly when used with PIVOT.VALUE and PIVOT.HEADER
    const dependencies = [];
    const coreDefinition = evalContext.getters.getPivotCoreDefinition(pivotId);
    if (coreDefinition.type === "SPREADSHEET" && coreDefinition.dataSet) {
        const { sheetId, zone } = coreDefinition.dataSet;
        const xc = zoneToXc(zone);
        const range = evalContext.getters.getRangeFromSheetXC(sheetId, xc);
        if (range === undefined || range.invalidXc || range.invalidSheetName) {
            throw new InvalidReferenceError();
        }
        if (evalContext.__originCellPosition &&
            range.sheetId === evalContext.__originSheetId &&
            isZoneInside(positionToZone(evalContext.__originCellPosition), zone)) {
            throw new CircularDependencyError();
        }
        dependencies.push(range);
    }
    for (const measure of forMeasures) {
        if (measure.computedBy) {
            dependencies.push(...evalContext.getters.getMeasureFullDependencies(pivotId, measure));
        }
    }
    const originPosition = evalContext.__originCellPosition;
    if (originPosition && dependencies.length) {
        // The following line is used to reset the dependencies of the cell, to avoid
        // keeping dependencies from previous evaluation of the PIVOT formula (i.e.
        // in case the reference has been changed).
        evalContext.updateDependencies?.(originPosition);
        evalContext.addDependencies?.(originPosition, dependencies);
    }
}

const DEFAULT_IS_SORTED = true;
const DEFAULT_MATCH_MODE = 0;
const DEFAULT_SEARCH_MODE = 1;
const DEFAULT_ABSOLUTE_RELATIVE_MODE = 1;
const A1_NOTATION_OPTIONS = [
    { value: true, label: _t("A1 style (default)") },
    { value: false, label: _t("R1C1 style") },
];
const IS_SORTED_OPTIONS = [
    { value: true, label: _t("Approximate match (default)") },
    { value: false, label: _t("Exact match") },
];
// -----------------------------------------------------------------------------
// ADDRESS
// -----------------------------------------------------------------------------
const ADDRESS = {
    description: _t("Returns a cell reference as a string. "),
    args: [
        arg("row (number)", _t("The row number of the cell reference. ")),
        arg("column (number)", _t("The column number (not name) of the cell reference. A is column number 1. ")),
        arg(`absolute_relative_mode (number, default=${DEFAULT_ABSOLUTE_RELATIVE_MODE})`, _t("An indicator of whether the reference is row/column absolute."), [
            { value: 1, label: _t("Absolute row and column (e.g. $A$1)") },
            { value: 2, label: _t("Absolute row, relative column (e.g. A$1)") },
            { value: 3, label: _t("Relative row, absolute column (e.g. $A1)") },
            { value: 4, label: _t("Relative row and column (e.g. A1)") },
        ]),
        arg("use_a1_notation (boolean, default=TRUE)", _t("A boolean indicating whether to use A1 style notation or R1C1 style notation."), A1_NOTATION_OPTIONS),
        arg("sheet (string, optional)", _t("A string indicating the name of the sheet into which the address points.")),
    ],
    compute: function (row, column, absoluteRelativeMode = { value: DEFAULT_ABSOLUTE_RELATIVE_MODE }, useA1Notation = { value: true }, sheet) {
        const rowNumber = strictToInteger(row, this.locale);
        const colNumber = strictToInteger(column, this.locale);
        if (rowNumber < 1) {
            return new EvaluationError(expectNumberGreaterThanOrEqualToOne(rowNumber));
        }
        if (colNumber < 1) {
            return new EvaluationError(expectNumberGreaterThanOrEqualToOne(colNumber));
        }
        const _absoluteRelativeMode = strictToInteger(absoluteRelativeMode, this.locale);
        if (![1, 2, 3, 4].includes(_absoluteRelativeMode)) {
            return new EvaluationError(expectNumberRangeError(1, 4, _absoluteRelativeMode));
        }
        const _useA1Notation = toBoolean(useA1Notation);
        let cellReference;
        if (_useA1Notation) {
            const rangePart = {
                rowFixed: [1, 2].includes(_absoluteRelativeMode),
                colFixed: [1, 3].includes(_absoluteRelativeMode),
            };
            cellReference = toXC(colNumber - 1, rowNumber - 1, rangePart);
        }
        else {
            const rowPart = [1, 2].includes(_absoluteRelativeMode) ? `R${rowNumber}` : `R[${rowNumber}]`;
            const colPart = [1, 3].includes(_absoluteRelativeMode) ? `C${colNumber}` : `C[${colNumber}]`;
            cellReference = rowPart + colPart;
        }
        if (sheet !== undefined) {
            return getFullReference(toString(sheet), cellReference);
        }
        return cellReference;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// COLUMN
// -----------------------------------------------------------------------------
const COLUMN = {
    description: _t("Column number of a specified cell."),
    args: [
        arg("cell_reference (meta, range<meta>, default='this cell')", _t("The cell whose column number will be returned. Column A corresponds to 1. By default, the function use the cell in which the formula is entered.")),
    ],
    compute: function (cellReference) {
        if (cellReference === undefined) {
            if (this.__originCellPosition?.col === undefined) {
                return new EvaluationError(_t("In this context, the function [[FUNCTION_NAME]] needs to have a cell or range in parameter."));
            }
            return this.__originCellPosition.col + 1;
        }
        if (cellReference[0][0] === undefined) {
            return new EvaluationError(_t("The range is out of bounds."));
        }
        if (cellReference[0][0].value === CellErrorType.InvalidReference) {
            return cellReference[0][0]; // return the same error
        }
        const left = this.getters.getRangeFromSheetXC(this.getters.getActiveSheetId(), cellReference[0][0].value).zone.left;
        if (cellReference.length === 1) {
            return left + 1;
        }
        return generateMatrix(cellReference.length, 1, (col, row) => ({ value: left + col + 1 }));
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// COLUMNS
// -----------------------------------------------------------------------------
const COLUMNS = {
    description: _t("Number of columns in a specified array or range."),
    args: [arg("range (any, range<any>)", _t("The range whose column count will be returned."))],
    compute: function (range) {
        const _range = toMatrix(range);
        if (_range[0][0] === undefined) {
            return new EvaluationError(_t("The range is out of bounds."));
        }
        if (_range[0][0].value === CellErrorType.InvalidReference) {
            return _range[0][0];
        }
        return _range.length;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// HLOOKUP
// -----------------------------------------------------------------------------
const HLOOKUP = {
    description: _t("Horizontal lookup"),
    args: [
        arg("search_key (string, number, boolean)", _t("The value to search for. For example, 42, 'Cats', or I24.")),
        arg("range (any, range)", _t("The range to consider for the search. The first row in the range is searched for the key specified in search_key.")),
        arg("index (number)", _t("The row index of the value to be returned, where the first row in range is numbered 1.")),
        arg(`is_sorted (boolean, default=${DEFAULT_IS_SORTED})`, _t("Indicates whether the row to be searched (the first row of the specified range) is sorted, in which case the closest match for search_key will be returned."), IS_SORTED_OPTIONS),
    ],
    compute: function (searchKey, range, index, isSorted = { value: DEFAULT_IS_SORTED }) {
        const _index = Math.trunc(toNumber(index?.value, this.locale));
        const _range = toMatrix(range);
        if (1 > _index || _index > _range[0].length) {
            return new EvaluationError(_t("[[FUNCTION_NAME]] evaluates to an out of bounds range."));
        }
        const getValueFromRange = (range, index) => range[index][0].value;
        const _isSorted = toBoolean(isSorted.value);
        const colIndex = _isSorted
            ? dichotomicSearch(_range, searchKey, "nextSmaller", "asc", _range.length, getValueFromRange)
            : linearSearch(_range, searchKey, "wildcard", _range.length, getValueFromRange, this.lookupCaches);
        const col = _range[colIndex];
        if (col === undefined) {
            return valueNotAvailable(searchKey);
        }
        return col[_index - 1];
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// INDEX
// -----------------------------------------------------------------------------
const INDEX = {
    description: _t("Returns the content of a cell, specified by row and column offset."),
    args: [
        arg("reference (any, range)", _t("The range of cells from which the values are returned.")),
        arg("row (number, default=0)", _t("The index of the row to be returned from within the reference range of cells.")),
        arg("column (number, default=0)", _t("The index of the column to be returned from within the reference range of cells.")),
    ],
    compute: function (reference, row = { value: 0 }, column = { value: 0 }) {
        const _reference = toMatrix(reference);
        const _row = toNumber(row.value, this.locale);
        const _column = toNumber(column.value, this.locale);
        if (_column < 0 ||
            _column - 1 >= _reference.length ||
            _row < 0 ||
            _row - 1 >= _reference[0].length) {
            return new EvaluationError(_t("Index out of range."));
        }
        if (_row === 0 && _column === 0) {
            return _reference;
        }
        if (_row === 0) {
            return [_reference[_column - 1]];
        }
        if (_column === 0) {
            return _reference.map((col) => [col[_row - 1]]);
        }
        return _reference[_column - 1][_row - 1];
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// INDIRECT
// -----------------------------------------------------------------------------
const INDIRECT = {
    description: _t("Returns the content of a cell, specified by a string."),
    args: [
        arg("reference (string)", _t("The range of cells from which the values are returned.")),
        arg("use_a1_notation (boolean, default=TRUE)", _t("A boolean indicating whether to use A1 style notation (TRUE) or R1C1 style notation (FALSE)."), A1_NOTATION_OPTIONS),
    ],
    compute: function (reference, useA1Notation = { value: true }) {
        const _reference = reference?.value?.toString();
        if (!_reference) {
            return new InvalidReferenceError(_t("Reference should be defined."));
        }
        const _useA1Notation = toBoolean(useA1Notation);
        if (!_useA1Notation) {
            return new EvaluationError(_t("R1C1 notation is not supported."));
        }
        const sheetId = this.__originSheetId;
        const originPosition = this.__originCellPosition;
        if (originPosition) {
            // The following line is used to reset the dependencies of the cell, to avoid
            // keeping dependencies from previous evaluation of the INDIRECT formula (i.e.
            // in case the reference has been changed).
            this.updateDependencies?.(originPosition);
        }
        const range = this.getters.getRangeFromSheetXC(sheetId, _reference);
        if (range === undefined || range.invalidXc || range.invalidSheetName) {
            return new InvalidReferenceError();
        }
        if (originPosition) {
            this.addDependencies?.(originPosition, [range]);
        }
        const values = [];
        for (let col = range.zone.left; col <= range.zone.right; col++) {
            const colValues = [];
            for (let row = range.zone.top; row <= range.zone.bottom; row++) {
                const position = { sheetId: range.sheetId, col, row };
                colValues.push(this.getters.getEvaluatedCell(position));
            }
            values.push(colValues);
        }
        return values.length === 1 && values[0].length === 1 ? values[0][0] : values;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// LOOKUP
// -----------------------------------------------------------------------------
const LOOKUP = {
    description: _t("Look up a value."),
    args: [
        arg("search_key (string, number, boolean)", _t("The value to search for. For example, 42, 'Cats', or I24.")),
        arg("search_array (any, range)", _t("One method of using this function is to provide a single sorted row or column search_array to look through for the search_key with a second argument result_range. The other way is to combine these two arguments into one search_array where the first row or column is searched and a value is returned from the last row or column in the array. If search_key is not found, a non-exact match may be returned.")),
        arg("result_range (any, range, optional)", _t("The range from which to return a result. The value returned corresponds to the location where search_key is found in search_range. This range must be only a single row or column and should not be used if using the search_result_array method.")),
    ],
    compute: function (searchKey, searchArray, resultRange) {
        const _searchArray = toMatrix(searchArray);
        const _resultRange = toMatrix(resultRange);
        let nbCol = _searchArray.length;
        let nbRow = _searchArray[0].length;
        const verticalSearch = nbRow >= nbCol;
        const getElement = verticalSearch
            ? (range, index) => range[0][index].value
            : (range, index) => range[index][0].value;
        const rangeLength = verticalSearch ? nbRow : nbCol;
        const index = dichotomicSearch(_searchArray, searchKey, "nextSmaller", "asc", rangeLength, getElement);
        if (index === -1 ||
            (verticalSearch && _searchArray[0][index] === undefined) ||
            (!verticalSearch && _searchArray[index][nbRow - 1] === undefined)) {
            return valueNotAvailable(searchKey);
        }
        if (_resultRange[0].length === 0) {
            return verticalSearch ? _searchArray[nbCol - 1][index] : _searchArray[index][nbRow - 1];
        }
        nbCol = _resultRange.length;
        nbRow = _resultRange[0].length;
        if (nbCol !== 1 && nbRow !== 1) {
            return new EvaluationError(_t("The result_range must be a single row or a single column."));
        }
        if (nbCol > 1) {
            if (index > nbCol - 1) {
                return new EvaluationError(_t("[[FUNCTION_NAME]] evaluates to an out of range row value %s.", index + 1));
            }
            return _resultRange[index][0];
        }
        if (index > nbRow - 1) {
            return new EvaluationError(_t("[[FUNCTION_NAME]] evaluates to an out of range column value %s.", index + 1));
        }
        return _resultRange[0][index];
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// MATCH
// -----------------------------------------------------------------------------
const DEFAULT_SEARCH_TYPE = 1;
const MATCH = {
    description: _t("Position of item in range that matches value."),
    args: [
        arg("search_key (string, number, boolean)", _t("The value to search for. For example, 42, 'Cats', or I24.")),
        arg("range (any, range)", _t("The one-dimensional array to be searched.")),
        arg(`search_type (number, default=${DEFAULT_SEARCH_TYPE})`, _t("The search method is a number 1, 0 or -1 indicating which value to return. 1 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."), [
            { value: 1, label: _t("Ascending order (default)") },
            { value: 0, label: _t("Exact match") },
            { value: -1, label: _t("Descending order") },
        ]),
    ],
    compute: function (searchKey, range, searchType = { value: DEFAULT_SEARCH_TYPE }) {
        let _searchType = toNumber(searchType, this.locale);
        const _range = toMatrix(range);
        const nbCol = _range.length;
        const nbRow = _range[0].length;
        if (nbCol !== 1 && nbRow !== 1) {
            return new EvaluationError(_t("The range must be a single row or a single column."));
        }
        let index = -1;
        const getElement = nbCol === 1
            ? (_range, index) => _range[0][index].value
            : (_range, index) => _range[index][0].value;
        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, "wildcard", rangeLen, getElement, this.lookupCaches);
                break;
            case -1:
                index = dichotomicSearch(_range, searchKey, "nextGreater", "desc", rangeLen, getElement);
                break;
        }
        if ((nbCol === 1 && _range[0][index] === undefined) ||
            (nbCol !== 1 && _range[index] === undefined)) {
            return valueNotAvailable(searchKey);
        }
        return index + 1;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// ROW
// -----------------------------------------------------------------------------
const ROW = {
    description: _t("Row number of a specified cell."),
    args: [
        arg("cell_reference (meta, range<meta>, default='this cell')", _t("The cell whose row number will be returned. By default, this function uses the cell in which the formula is entered.")),
    ],
    compute: function (cellReference) {
        if (cellReference === undefined) {
            if (this.__originCellPosition?.row === undefined) {
                return new EvaluationError(_t("In this context, the function [[FUNCTION_NAME]] needs to have a cell or range in parameter."));
            }
            return this.__originCellPosition.row + 1;
        }
        if (cellReference[0][0] === undefined) {
            return new EvaluationError(_t("The range is out of bounds."));
        }
        if (cellReference[0][0].value === CellErrorType.InvalidReference) {
            return cellReference[0][0]; // return the same error
        }
        const top = this.getters.getRangeFromSheetXC(this.getters.getActiveSheetId(), cellReference[0][0].value).zone.top;
        if (cellReference[0].length === 1) {
            return top + 1;
        }
        return generateMatrix(1, cellReference[0].length, (col, row) => ({ value: top + row + 1 }));
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// ROWS
// -----------------------------------------------------------------------------
const ROWS = {
    description: _t("Number of rows in a specified array or range."),
    args: [arg("range (any, range<any>)", _t("The range whose row count will be returned."))],
    compute: function (range) {
        const _range = toMatrix(range);
        if (_range[0][0] === undefined) {
            return new EvaluationError(_t("The range is out of bounds."));
        }
        if (_range[0][0].value === CellErrorType.InvalidReference) {
            return _range[0][0];
        }
        return _range[0].length;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// VLOOKUP
// -----------------------------------------------------------------------------
const VLOOKUP = {
    description: _t("Vertical lookup."),
    args: [
        arg("search_key (string, number, boolean)", _t("The value to search for. For example, 42, 'Cats', or I24.")),
        arg("range (any, range)", _t("The range to consider for the search. The first column in the range is searched for the key specified in search_key.")),
        arg("index (number)", _t("The column index of the value to be returned, where the first column in range is numbered 1.")),
        arg(`is_sorted (boolean, default=${DEFAULT_IS_SORTED})`, _t("Indicates whether the column to be searched (the first column of the specified range) is sorted, in which case the closest match for search_key will be returned."), IS_SORTED_OPTIONS),
    ],
    compute: function (searchKey, range, index, isSorted = { value: DEFAULT_IS_SORTED }) {
        const _index = Math.trunc(toNumber(index?.value, this.locale));
        const _range = toMatrix(range);
        if (1 > _index || _index > _range.length) {
            return new EvaluationError(_t("[[FUNCTION_NAME]] evaluates to an out of bounds range."));
        }
        const getValueFromRange = (range, index) => range[0][index].value;
        const _isSorted = toBoolean(isSorted.value);
        const rowIndex = _isSorted
            ? dichotomicSearch(_range, searchKey, "nextSmaller", "asc", _range[0].length, getValueFromRange)
            : linearSearch(_range, searchKey, "wildcard", _range[0].length, getValueFromRange, this.lookupCaches);
        const value = _range[_index - 1][rowIndex];
        if (value === undefined) {
            return valueNotAvailable(searchKey);
        }
        return value;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// XLOOKUP
// -----------------------------------------------------------------------------
const MATCH_MODE = {
    "0": "strict",
    "1": "nextGreater",
    "-1": "nextSmaller",
    "2": "wildcard",
};
const XLOOKUP = {
    description: _t("Search a range for a match and return the corresponding item from a second range."),
    args: [
        arg("search_key (string,number,boolean)", _t("The value to search for.")),
        arg("lookup_range (any, range)", _t("The range to consider for the search. Should be a single column or a single row.")),
        arg("return_range (any, range)", _t("The range containing the return value. Should have the same dimensions as lookup_range.")),
        arg("if_not_found (any, optional)", _t("If a valid match is not found, return this value.")),
        arg(`match_mode (any, default=${DEFAULT_MATCH_MODE})`, _t("Specifies how to match search_key with the items in lookup_range. "), [
            { value: 0, label: _t("Exact match (default)") },
            { value: -1, label: _t("Exact match or next smaller item") },
            { value: 1, label: _t("Exact match or next larger item") },
            { value: 2, label: _t("Wildcard character match") },
        ]),
        arg(`search_mode (any, default=${DEFAULT_SEARCH_MODE})`, _t("Specifies the search mode to use. By default, a first to last search will be used."), [
            { value: 1, label: _t("Search first to last (default)") },
            { value: -1, label: _t("Search last to first") },
            { value: 2, label: _t("Binary search (sorted ascending order)") },
            { value: -2, label: _t("Binary search (sorted descending order)") },
        ]),
    ],
    compute: function (searchKey, lookupRange, returnRange, defaultValue, matchMode = { value: DEFAULT_MATCH_MODE }, searchMode = { value: DEFAULT_SEARCH_MODE }) {
        const _matchMode = Math.trunc(toNumber(matchMode.value, this.locale));
        const _searchMode = Math.trunc(toNumber(searchMode.value, this.locale));
        const _lookupRange = toMatrix(lookupRange);
        const _returnRange = toMatrix(returnRange);
        if (_lookupRange.length !== 1 && _lookupRange[0].length !== 1) {
            return new EvaluationError(_t("lookup_range should be either a single row or single column."));
        }
        if (![1, -1, 2, -2].includes(_searchMode)) {
            return new EvaluationError(_t("search_mode should be a value in [-1, 1, -2, 2]."));
        }
        if (![-1, 0, 1, 2].includes(_matchMode)) {
            return new EvaluationError(_t("match_mode should be a value in [-1, 0, 1, 2]."));
        }
        const lookupDirection = _lookupRange.length === 1 ? "col" : "row";
        if (_matchMode === 2 && [-2, 2].includes(_searchMode)) {
            return new EvaluationError(_t("The search and match mode combination is not supported for XLOOKUP evaluation."));
        }
        if (lookupDirection === "col"
            ? _returnRange[0].length !== _lookupRange[0].length
            : _returnRange.length !== _lookupRange.length) {
            return new EvaluationError(_t("return_range should have the same dimensions as lookup_range."));
        }
        const getElement = lookupDirection === "col"
            ? (range, index) => range[0][index].value
            : (range, index) => range[index][0].value;
        const rangeLen = lookupDirection === "col" ? _lookupRange[0].length : _lookupRange.length;
        const mode = MATCH_MODE[_matchMode];
        const reverseSearch = _searchMode === -1;
        const index = _searchMode === 2 || _searchMode === -2
            ? dichotomicSearch(_lookupRange, searchKey, mode, _searchMode === 2 ? "asc" : "desc", rangeLen, getElement)
            : linearSearch(_lookupRange, searchKey, mode, rangeLen, getElement, this.lookupCaches, reverseSearch);
        if (index !== -1) {
            return lookupDirection === "col"
                ? _returnRange.map((col) => [col[index]])
                : [_returnRange[index]];
        }
        if (defaultValue === undefined) {
            return valueNotAvailable(searchKey);
        }
        return [[defaultValue]];
    },
    isExported: true,
};
//--------------------------------------------------------------------------
// Pivot functions
//--------------------------------------------------------------------------
// PIVOT.VALUE
const PIVOT_VALUE = {
    description: _t("Get the value from a pivot."),
    args: [
        arg("pivot_id (number,string)", _t("ID of the pivot.")),
        arg("measure_name (string)", _t("Name of the measure.")),
        arg("domain_field_name (string,repeating)", _t("Field name.")),
        arg("domain_value (number,string,boolean,repeating)", _t("Value.")),
    ],
    compute: function (formulaId, measureName, ...domainArgs) {
        const _pivotFormulaId = toString(formulaId);
        const _measure = toString(measureName);
        const pivotId = getPivotId(_pivotFormulaId, this.getters);
        assertMeasureExist(pivotId, _measure, this.getters);
        assertDomainLength(domainArgs);
        const pivot = this.getters.getPivot(pivotId);
        const coreDefinition = this.getters.getPivotCoreDefinition(pivotId);
        addPivotDependencies(this, pivotId, coreDefinition.measures.filter((m) => m.id === _measure));
        pivot.init({ reload: pivot.needsReevaluation });
        const error = pivot.assertIsValid({ throwOnError: false });
        if (error) {
            return error;
        }
        if (!pivot.areDomainArgsFieldsValid(domainArgs)) {
            const suggestion = _t("Consider using a dynamic pivot formula: %s. Or re-insert the static pivot from the Data menu.", `=PIVOT(${_pivotFormulaId})`);
            return {
                value: CellErrorType.GenericError,
                message: _t("Dimensions don't match the pivot definition") + ". " + suggestion,
            };
        }
        const domain = pivot.parseArgsToPivotDomain(domainArgs);
        if (this.getters.getActiveSheetId() === this.__originSheetId) {
            this.getters.getPivotPresenceTracker(pivotId)?.trackValue(_measure, domain);
        }
        return pivot.getPivotCellValueAndFormat(_measure, domain);
    },
};
// PIVOT.HEADER
const PIVOT_HEADER = {
    description: _t("Get the header of a pivot."),
    args: [
        arg("pivot_id (number,string)", _t("ID of the pivot.")),
        arg("domain_field_name (string,repeating)", _t("Field name.")),
        arg("domain_value (number,string,value,repeating)", _t("Value.")),
    ],
    compute: function (pivotId, ...domainArgs) {
        const _pivotFormulaId = toString(pivotId);
        const _pivotId = getPivotId(_pivotFormulaId, this.getters);
        assertDomainLength(domainArgs);
        const pivot = this.getters.getPivot(_pivotId);
        addPivotDependencies(this, _pivotId, []);
        pivot.init({ reload: pivot.needsReevaluation });
        const error = pivot.assertIsValid({ throwOnError: false });
        if (error) {
            return error;
        }
        if (!pivot.areDomainArgsFieldsValid(domainArgs)) {
            const suggestion = _t("Consider using a dynamic pivot formula: %s. Or re-insert the static pivot from the Data menu.", `=PIVOT(${_pivotFormulaId})`);
            return {
                value: CellErrorType.GenericError,
                message: _t("Dimensions don't match the pivot definition") + ". " + suggestion,
            };
        }
        const domain = pivot.parseArgsToPivotDomain(domainArgs);
        if (this.getters.getActiveSheetId() === this.__originSheetId) {
            this.getters.getPivotPresenceTracker(_pivotId)?.trackHeader(domain);
        }
        const lastNode = domain.at(-1);
        if (lastNode?.field === "measure") {
            return pivot.getPivotMeasureValue(toString(lastNode.value), domain);
        }
        const { value, format } = pivot.getPivotHeaderValueAndFormat(domain);
        return {
            value,
            format: !lastNode || lastNode.field === "measure" || lastNode.value === "false"
                ? undefined
                : format,
        };
    },
};
const PIVOT = {
    description: _t("Get a pivot table."),
    args: [
        arg("pivot_id (string)", _t("ID of the pivot.")),
        arg("row_count (number, optional)", _t("number of rows")),
        arg("include_total (boolean, default=TRUE)", _t("Whether to include total/sub-totals or not.")),
        arg("include_column_titles (boolean, default=TRUE)", _t("Whether to include the column titles or not.")),
        arg("column_count (number, optional)", _t("number of columns")),
        arg("include_measure_titles (boolean, default=TRUE)", _t("Whether to include the measure titles row or not.")),
    ],
    compute: function (pivotFormulaId, rowCount = { value: 10000 }, includeTotal = { value: true }, includeColumnHeaders = { value: true }, columnCount = { value: Number.MAX_VALUE }, includeMeasureTitles = { value: true }) {
        const _pivotFormulaId = toString(pivotFormulaId);
        const _rowCount = toNumber(rowCount, this.locale);
        if (_rowCount < 0) {
            return new EvaluationError(_t("The number of rows must be positive."));
        }
        const _columnCount = toNumber(columnCount, this.locale);
        if (_columnCount < 0) {
            return new EvaluationError(_t("The number of columns must be positive."));
        }
        const visibilityOptions = {
            displayColumnHeaders: toBoolean(includeColumnHeaders),
            displayTotals: toBoolean(includeTotal),
            displayMeasuresRow: toBoolean(includeMeasureTitles),
        };
        const pivotId = getPivotId(_pivotFormulaId, this.getters);
        const pivot = this.getters.getPivot(pivotId);
        const coreDefinition = this.getters.getPivotCoreDefinition(pivotId);
        addPivotDependencies(this, pivotId, coreDefinition.measures);
        pivot.init({ reload: pivot.needsReevaluation });
        const error = pivot.assertIsValid({ throwOnError: false });
        if (error) {
            return error;
        }
        const table = pivot.getCollapsedTableStructure();
        if (table.numberOfCells > PIVOT_MAX_NUMBER_OF_CELLS) {
            return new EvaluationError(getPivotTooBigErrorMessage(table.numberOfCells, this.locale));
        }
        const cells = table.getPivotCells(visibilityOptions);
        let headerRows = 0;
        if (visibilityOptions.displayColumnHeaders) {
            headerRows = table.columns.length - 1;
        }
        if (visibilityOptions.displayMeasuresRow) {
            headerRows++;
        }
        const pivotTitle = this.getters.getPivotName(pivotId);
        const tableHeight = Math.min(headerRows + _rowCount, cells[0].length);
        if (tableHeight === 0) {
            return [[{ value: pivotTitle }]];
        }
        const tableWidth = Math.min(1 + _columnCount, cells.length);
        const result = [];
        for (const col of range(0, tableWidth)) {
            result[col] = [];
            for (const row of range(0, tableHeight)) {
                const pivotCell = cells[col][row];
                switch (pivotCell.type) {
                    case "EMPTY":
                        result[col].push({ value: "" });
                        break;
                    case "HEADER":
                        const valueAndFormat = pivot.getPivotHeaderValueAndFormat(pivotCell.domain);
                        result[col].push(addAlignFormatToPivotHeader(pivotCell.domain, valueAndFormat));
                        break;
                    case "MEASURE_HEADER":
                        result[col].push(pivot.getPivotMeasureValue(pivotCell.measure, pivotCell.domain));
                        break;
                    case "VALUE":
                        result[col].push(pivot.getPivotCellValueAndFormat(pivotCell.measure, pivotCell.domain));
                        break;
                }
            }
        }
        if (visibilityOptions.displayColumnHeaders || visibilityOptions.displayMeasuresRow) {
            result[0][0] = { value: pivotTitle };
        }
        return result;
    },
};
//--------------------------------------------------------------------------
// OFFSET
//--------------------------------------------------------------------------
const OFFSET = {
    description: _t("Returns a range reference shifted by a specified number of rows and columns from a starting cell reference."),
    args: [
        arg("cell_reference (meta, range<meta>)", _t("The starting point from which to count the offset rows and columns.")),
        arg("offset_rows (number)", _t("The number of rows to offset by.")),
        arg("offset_columns (number)", _t("The number of columns to offset by.")),
        arg("height (number, default='height of cell_reference')", _t("The number of rows of the range to return starting at the offset target.")),
        arg("width (number, default='width of cell_reference')", _t("The number of columns of the range to return starting at the offset target.")),
    ],
    compute: function (cellReference, offsetRows, offsetColumns, height, width) {
        if (isEvaluationError(cellReference[0][0].value)) {
            return cellReference[0][0];
        }
        const ref0 = cellReference[0][0].value;
        if (!ref0) {
            return new EvaluationError("In this context, the function OFFSET needs to have a cell or range in parameter.");
        }
        const zone = toZone(ref0);
        let offsetHeight = cellReference[0].length;
        let offsetWidth = cellReference.length;
        if (height) {
            const _height = toNumber(height, this.locale);
            if (_height < 1) {
                return new EvaluationError(_t("Height value is %(_height)s. It should be greater than or equal to 1.", { _height }));
            }
            offsetHeight = _height;
        }
        if (width) {
            const _width = toNumber(width, this.locale);
            if (_width < 1) {
                return new EvaluationError(_t("Width value is %(_width)s. It should be greater than or equal to 1.", { _width }));
            }
            offsetWidth = _width;
        }
        const { sheetName } = splitReference(ref0);
        const sheetId = (sheetName && this.getters.getSheetIdByName(sheetName)) || this.getters.getActiveSheetId();
        const _offsetRows = toNumber(offsetRows, this.locale);
        const _offsetColumns = toNumber(offsetColumns, this.locale);
        const originPosition = this.__originCellPosition;
        if (originPosition) {
            this.updateDependencies?.(originPosition);
        }
        const startingCol = zone.left + _offsetColumns;
        const startingRow = zone.top + _offsetRows;
        if (startingCol < 0 || startingRow < 0) {
            return new InvalidReferenceError(_t("OFFSET evaluates to an out of bounds range."));
        }
        const dependencyZone = {
            left: startingCol,
            top: startingRow,
            right: startingCol + offsetWidth - 1,
            bottom: startingRow + offsetHeight - 1,
        };
        const range = this.getters.getRangeFromZone(sheetId, dependencyZone);
        if (range.invalidXc || range.invalidSheetName) {
            return new InvalidReferenceError();
        }
        if (originPosition) {
            this.addDependencies?.(originPosition, [range]);
        }
        return generateMatrix(offsetWidth, offsetHeight, (col, row) => this.getters.getEvaluatedCell({
            sheetId,
            col: startingCol + col,
            row: startingRow + row,
        }));
    },
};

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

// -----------------------------------------------------------------------------
// ADD
// -----------------------------------------------------------------------------
const ADD = {
    description: _t("Sum of two numbers."),
    args: [
        arg("value1 (number)", _t("The first addend.")),
        arg("value2 (number)", _t("The second addend.")),
    ],
    compute: function (value1, value2) {
        return {
            value: toNumber(value1, this.locale) + toNumber(value2, this.locale),
            format: value1?.format || value2?.format,
        };
    },
};
// -----------------------------------------------------------------------------
// CONCAT
// -----------------------------------------------------------------------------
const CONCAT = {
    description: _t("Concatenation of two values."),
    args: [
        arg("value1 (string)", _t("The value to which value2 will be appended.")),
        arg("value2 (string)", _t("The value to append to value1.")),
    ],
    compute: function (value1, value2) {
        return toString(value1) + toString(value2);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// DIVIDE
// -----------------------------------------------------------------------------
const DIVIDE = {
    description: _t("One number divided by another."),
    args: [
        arg("dividend (number)", _t("The number to be divided.")),
        arg("divisor (number)", _t("The number to divide by.")),
    ],
    compute: function (dividend, divisor) {
        const _divisor = toNumber(divisor, this.locale);
        if (_divisor === 0) {
            return new DivisionByZeroError(_t("The divisor must be different from zero."));
        }
        return {
            value: toNumber(dividend, this.locale) / _divisor,
            format: dividend?.format || divisor?.format,
        };
    },
};
// -----------------------------------------------------------------------------
// EQ
// -----------------------------------------------------------------------------
function isEmpty(data) {
    return data === undefined || data.value === null;
}
const getNeutral = { number: 0, string: "", boolean: false };
function areAlmostEqual(value1, value2, epsilon = 2e-16) {
    return Math.abs(value1 - value2) < epsilon;
}
const EQ = {
    description: _t("Equal."),
    args: [
        arg("value1 (string, number, boolean)", _t("The first value.")),
        arg("value2 (string, number, boolean)", _t("The value to test against value1 for equality.")),
    ],
    compute: function (value1, value2) {
        if (isEvaluationError(value1?.value)) {
            return value1;
        }
        if (isEvaluationError(value2?.value)) {
            return value2;
        }
        let _value1 = isEmpty(value1) ? getNeutral[typeof value2?.value] : value1?.value;
        let _value2 = isEmpty(value2) ? getNeutral[typeof value1?.value] : value2?.value;
        if (typeof _value1 === "string") {
            _value1 = _value1.toUpperCase();
        }
        if (typeof _value2 === "string") {
            _value2 = _value2.toUpperCase();
        }
        if (typeof _value1 === "number" && typeof _value2 === "number") {
            return { value: areAlmostEqual(_value1, _value2) };
        }
        return { value: _value1 === _value2 };
    },
};
// -----------------------------------------------------------------------------
// GT
// -----------------------------------------------------------------------------
function applyRelationalOperator(value1, value2, cb) {
    if (isEvaluationError(value1?.value)) {
        return value1;
    }
    if (isEvaluationError(value2?.value)) {
        return value2;
    }
    let _value1 = isEmpty(value1) ? getNeutral[typeof value2?.value] : value1?.value;
    let _value2 = isEmpty(value2) ? getNeutral[typeof value1?.value] : value2?.value;
    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 { value: true };
    }
    if (tV2 === "string" && tV1 === "number") {
        return { value: false };
    }
    return { value: cb(_value1, _value2) };
}
const GT = {
    description: _t("Strictly greater than."),
    args: [
        arg("value1 (number, string, boolean)", _t("The value to test as being greater than value2.")),
        arg("value2 (number, string, boolean)", _t("The second value.")),
    ],
    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: _t("Greater than or equal to."),
    args: [
        arg("value1 (number, string, boolean)", _t("The value to test as being greater than or equal to value2.")),
        arg("value2 (number, string, boolean)", _t("The second value.")),
    ],
    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: _t("Less than."),
    args: [
        arg("value1 (number, string, boolean)", _t("The value to test as being less than value2.")),
        arg("value2 (number, string, boolean)", _t("The second value.")),
    ],
    compute: function (value1, value2) {
        const result = GTE.compute.bind(this)(value1, value2);
        if (isEvaluationError(result.value)) {
            return result;
        }
        return { value: !result.value };
    },
};
// -----------------------------------------------------------------------------
// LTE
// -----------------------------------------------------------------------------
const LTE = {
    description: _t("Less than or equal to."),
    args: [
        arg("value1 (number, string, boolean)", _t("The value to test as being less than or equal to value2.")),
        arg("value2 (number, string, boolean)", _t("The second value.")),
    ],
    compute: function (value1, value2) {
        const result = GT.compute.bind(this)(value1, value2);
        if (isEvaluationError(result.value)) {
            return result;
        }
        return { value: !result.value };
    },
};
// -----------------------------------------------------------------------------
// MINUS
// -----------------------------------------------------------------------------
const MINUS = {
    description: _t("Difference of two numbers."),
    args: [
        arg("value1 (number)", _t("The minuend, or number to be subtracted from.")),
        arg("value2 (number)", _t("The subtrahend, or number to subtract from value1.")),
    ],
    compute: function (value1, value2) {
        return {
            value: toNumber(value1, this.locale) - toNumber(value2, this.locale),
            format: value1?.format || value2?.format,
        };
    },
};
// -----------------------------------------------------------------------------
// MULTIPLY
// -----------------------------------------------------------------------------
const MULTIPLY = {
    description: _t("Product of two numbers"),
    args: [
        arg("factor1 (number)", _t("The first multiplicand.")),
        arg("factor2 (number)", _t("The second multiplicand.")),
    ],
    compute: function (factor1, factor2) {
        return {
            value: toNumber(factor1, this.locale) * toNumber(factor2, this.locale),
            format: factor1?.format || factor2?.format,
        };
    },
};
// -----------------------------------------------------------------------------
// NE
// -----------------------------------------------------------------------------
const NE = {
    description: _t("Not equal."),
    args: [
        arg("value1 (string, number, boolean)", _t("The first value.")),
        arg("value2 (string, number, boolean)", _t("The value to test against value1 for inequality.")),
    ],
    compute: function (value1, value2) {
        const result = EQ.compute.bind(this)(value1, value2);
        if (isEvaluationError(result.value)) {
            return result;
        }
        return { value: !result.value };
    },
};
// -----------------------------------------------------------------------------
// POW
// -----------------------------------------------------------------------------
const POW = {
    description: _t("A number raised to a power."),
    args: [
        arg("base (number)", _t("The number to raise to the exponent power.")),
        arg("exponent (number)", _t("The exponent to raise base to.")),
    ],
    compute: function (base, exponent) {
        return POWER.compute.bind(this)(base, exponent);
    },
};
// -----------------------------------------------------------------------------
// UMINUS
// -----------------------------------------------------------------------------
const UMINUS = {
    description: _t("A number with the sign reversed."),
    args: [
        arg("value (number)", _t("The number to have its sign reversed. Equivalently, the number to multiply by -1.")),
    ],
    compute: function (value) {
        return {
            value: -toNumber(value, this.locale),
            format: value?.format,
        };
    },
};
// -----------------------------------------------------------------------------
// UNARY_PERCENT
// -----------------------------------------------------------------------------
const UNARY_PERCENT = {
    description: _t("Value interpreted as a percentage."),
    args: [arg("percentage (number)", _t("The value to interpret as a percentage."))],
    compute: function (percentage) {
        return toNumber(percentage, this.locale) / 100;
    },
};
// -----------------------------------------------------------------------------
// UPLUS
// -----------------------------------------------------------------------------
const UPLUS = {
    description: _t("A specified number, unchanged."),
    args: [arg("value (any)", _t("The number to return."))],
    compute: function (value = { value: null }) {
        return 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 transformFromFactor = (factor) => ({
    transform: (x) => x * factor,
    inverseTransform: (x) => x / factor,
});
const standard = { transform: (x) => x, inverseTransform: (x) => x };
const ANG2M = 1e-10;
const IN2M = 0.0254;
const PICAPT2M = IN2M / 72;
const FT2M = 0.3048;
const YD2M = 0.9144;
const MI2M = 1609.34;
const NMI2M = 1852;
const LY2M = 9.46073047258e15;
const UNITS = {
    // WEIGHT UNITs : Standard = gramme
    g: { ...standard, category: "weight" },
    u: { ...transformFromFactor(1.66053e-24), category: "weight" },
    grain: { ...transformFromFactor(0.0647989), category: "weight" },
    ozm: { ...transformFromFactor(28.3495), category: "weight" },
    lbm: { ...transformFromFactor(453.592), category: "weight" },
    stone: { ...transformFromFactor(6350.29), category: "weight" },
    sg: { ...transformFromFactor(14593.90294), category: "weight" },
    cwt: { ...transformFromFactor(45359.237), category: "weight" },
    uk_cwt: { ...transformFromFactor(50802.3), category: "weight" },
    ton: { ...transformFromFactor(907184.74), category: "weight" },
    uk_ton: { ...transformFromFactor(1016046.9), category: "weight" },
    // DISTANCE UNITS : Standard = meter
    m: { ...standard, category: "distance" },
    km: { ...transformFromFactor(1000), category: "distance" },
    ang: { ...transformFromFactor(ANG2M), category: "distance" },
    Picapt: { ...transformFromFactor(PICAPT2M), category: "distance" },
    pica: { ...transformFromFactor(IN2M / 6), category: "distance" },
    in: { ...transformFromFactor(IN2M), category: "distance" },
    ft: { ...transformFromFactor(FT2M), category: "distance" },
    yd: { ...transformFromFactor(YD2M), category: "distance" },
    ell: { ...transformFromFactor(1.143), category: "distance" },
    mi: { ...transformFromFactor(MI2M), category: "distance" },
    survey_mi: { ...transformFromFactor(1609.34), category: "distance" },
    Nmi: { ...transformFromFactor(NMI2M), category: "distance" },
    ly: { ...transformFromFactor(LY2M), category: "distance" },
    parsec: { ...transformFromFactor(3.0856775814914e16), category: "distance" },
    // TIME UNITS : Standard = second
    sec: { ...standard, category: "time" },
    min: { ...transformFromFactor(60), category: "time" },
    hr: { ...transformFromFactor(3600), category: "time" },
    day: { ...transformFromFactor(86400), category: "time" },
    yr: { ...transformFromFactor(31556952), category: "time" },
    // PRESSURE UNITS : Standard = Pascal
    Pa: { ...standard, category: "pressure" },
    bar: { ...transformFromFactor(100000), category: "pressure" },
    mmHg: { ...transformFromFactor(133.322), category: "pressure" },
    Torr: { ...transformFromFactor(133.322), category: "pressure" },
    psi: { ...transformFromFactor(6894.76), category: "pressure" },
    atm: { ...transformFromFactor(101325), category: "pressure" },
    // FORCE UNITS : Standard = Newton
    N: { ...standard, category: "force" },
    dyn: { ...transformFromFactor(1e-5), category: "force" },
    pond: { ...transformFromFactor(0.00980665), category: "force" },
    lbf: { ...transformFromFactor(4.44822), category: "force" },
    // ENERGY UNITS : Standard = Joule
    J: { ...standard, category: "energy" },
    eV: { ...transformFromFactor(1.60218e-19), category: "energy" },
    e: { ...transformFromFactor(1e-7), category: "energy" },
    flb: { ...transformFromFactor(1.3558179483), category: "energy" },
    c: { ...transformFromFactor(4.184), category: "energy" },
    cal: { ...transformFromFactor(4.1868), category: "energy" },
    BTU: { ...transformFromFactor(1055.06), category: "energy" },
    Wh: { ...transformFromFactor(3600), category: "energy" },
    HPh: { ...transformFromFactor(2684520), category: "energy" },
    // POWER UNITS : Standard = Watt
    W: { ...standard, category: "power" },
    PS: { ...transformFromFactor(735.499), category: "power" },
    HP: { ...transformFromFactor(745.7), category: "power" },
    // MAGNETISM UNITS : Standard = Tesla
    T: { ...standard, category: "magnetism" },
    ga: { ...transformFromFactor(1e-4), category: "magnetism" },
    // TEMPERATURE UNITS : Standard = Kelvin
    K: { ...standard, category: "temperature" },
    C: {
        transform: (T) => T + 273.15,
        inverseTransform: (T) => T - 273.15,
        category: "temperature",
    },
    F: {
        transform: (T) => ((T - 32) * 5) / 9 + 273.15,
        inverseTransform: (T) => ((T - 273.15) * 9) / 5 + 32,
        category: "temperature",
    },
    Rank: { ...transformFromFactor(5 / 9), category: "temperature" },
    Reau: {
        transform: (T) => T * 1.25 + 273.15,
        inverseTransform: (T) => (T - 273.15) / 1.25,
        category: "temperature",
    },
    // VOLUME UNITS : Standard = cubic meter
    "m^3": { ...standard, category: "volume", order: 3 },
    "ang^3": { ...transformFromFactor(Math.pow(ANG2M, 3)), category: "volume", order: 3 },
    "Picapt^3": { ...transformFromFactor(Math.pow(PICAPT2M, 3)), category: "volume", order: 3 },
    tsp: { ...transformFromFactor(4.92892e-6), category: "volume" },
    tspm: { ...transformFromFactor(5e-6), category: "volume" },
    tbs: { ...transformFromFactor(1.4786764825785619e-5), category: "volume" },
    "in^3": { ...transformFromFactor(Math.pow(IN2M, 3)), category: "volume", order: 3 },
    oz: { ...transformFromFactor(2.95735295625e-5), category: "volume" },
    cup: { ...transformFromFactor(0.000237), category: "volume" },
    pt: { ...transformFromFactor(0.0004731765), category: "volume" },
    uk_pt: { ...transformFromFactor(0.000568261), category: "volume" },
    qt: { ...transformFromFactor(0.0009463529), category: "volume" },
    l: { ...transformFromFactor(1e-3), category: "volume" },
    uk_qt: { ...transformFromFactor(0.0011365225), category: "volume" },
    gal: { ...transformFromFactor(0.0037854118), category: "volume" },
    uk_gal: { ...transformFromFactor(0.00454609), category: "volume" },
    "ft^3": { ...transformFromFactor(Math.pow(FT2M, 3)), category: "volume", order: 3 },
    bushel: { ...transformFromFactor(0.0352390704), category: "volume" },
    barrel: { ...transformFromFactor(0.158987295), category: "volume" },
    "yd^3": { ...transformFromFactor(Math.pow(YD2M, 3)), category: "volume", order: 3 },
    MTON: { ...transformFromFactor(1.13267386368), category: "volume" },
    GRT: { ...transformFromFactor(2.83168), category: "volume" },
    "mi^3": { ...transformFromFactor(Math.pow(MI2M, 3)), category: "volume", order: 3 },
    "Nmi^3": { ...transformFromFactor(Math.pow(NMI2M, 3)), category: "volume", order: 3 },
    "ly^3": { ...transformFromFactor(Math.pow(LY2M, 3)), category: "volume", order: 3 },
    // AREA UNITS : Standard = square meter
    "m^2": { ...standard, category: "area", order: 2 },
    "ang^2": { ...transformFromFactor(Math.pow(ANG2M, 2)), category: "area", order: 2 },
    "Picapt^2": { ...transformFromFactor(Math.pow(PICAPT2M, 2)), category: "area", order: 2 },
    "in^2": { ...transformFromFactor(Math.pow(IN2M, 2)), category: "area", order: 2 },
    "ft^2": { ...transformFromFactor(Math.pow(FT2M, 2)), category: "area", order: 2 },
    "yd^2": { ...transformFromFactor(Math.pow(YD2M, 2)), category: "area", order: 2 },
    ar: { ...transformFromFactor(100), category: "area" },
    Morgen: { ...transformFromFactor(2500), category: "area" },
    uk_acre: { ...transformFromFactor(4046.8564224), category: "area" },
    us_acre: { ...transformFromFactor(4046.8726098743), category: "area" },
    ha: { ...transformFromFactor(1e4), category: "area" },
    "mi^2": { ...transformFromFactor(Math.pow(MI2M, 2)), category: "area", order: 2 },
    "Nmi^2": { ...transformFromFactor(Math.pow(NMI2M, 2)), category: "area", order: 2 },
    "ly^2": { ...transformFromFactor(Math.pow(LY2M, 2)), category: "area", order: 2 },
    // INFORMATION UNITS : Standard = bit
    bit: { ...standard, category: "information" },
    byte: { ...transformFromFactor(8), category: "information" },
    // SPEED UNITS : Standard = m/s
    "m/s": { ...standard, category: "speed" },
    "m/hr": { ...transformFromFactor(1 / 3600), category: "speed" },
    "km/hr": { ...transformFromFactor(1 / 3.6), category: "speed" },
    mph: { ...transformFromFactor(0.44704), category: "speed" },
    kn: { ...transformFromFactor(0.5144444444), category: "speed" },
    admkn: { ...transformFromFactor(0.5147733333), category: "speed" },
};
const UNITS_ALIASES = {
    shweight: "cwt",
    lcwt: "uk_cwt",
    hweight: "uk_cwt",
    LTON: "uk_ton",
    brton: "uk_ton",
    pc: "parsec",
    Pica: "Picapt",
    d: "day",
    mn: "min",
    s: "sec",
    p: "Pa",
    at: "atm",
    dy: "dyn",
    ev: "eV",
    hh: "HPh",
    wh: "Wh",
    btu: "BTU",
    h: "HP",
    cel: "C",
    fah: "F",
    kel: "K",
    us_pt: "pt",
    L: "l",
    lt: "l",
    ang3: "ang^3",
    ft3: "ft^3",
    in3: "in^3",
    ly3: "ly^3",
    m3: "m^3",
    mi3: "mi^3",
    yd3: "yd^3",
    Nmi3: "Nmi^3",
    Picapt3: "Picapt^3",
    "Pica^3": "Picapt^3",
    Pica3: "Picapt^3",
    regton: "GRT",
    ang2: "ang^2",
    ft2: "ft^2",
    in2: "in^2",
    ly2: "ly^2",
    m2: "m^2",
    mi2: "mi^2",
    Nmi2: "Nmi^2",
    Picapt2: "Picapt^2",
    "Pica^2": "Picapt^2",
    Pica2: "Picapt^2",
    yd2: "yd^2",
    "m/h": "m/hr",
    "m/sec": "m/s",
};
const UNIT_PREFIXES = {
    "": 1,
    Y: 1e24,
    Z: 1e21,
    E: 1e18,
    P: 1e15,
    T: 1e12,
    G: 1e9,
    M: 1e6,
    k: 1e3,
    h: 1e2,
    da: 1e1,
    e: 1e1,
    d: 1e-1,
    c: 1e-2,
    m: 1e-3,
    u: 1e-6,
    n: 1e-9,
    p: 1e-12,
    f: 1e-15,
    a: 1e-18,
    z: 1e-21,
    y: 1e-21,
    Yi: Math.pow(2, 80),
    Zi: Math.pow(2, 70),
    Ei: Math.pow(2, 60),
    Pi: Math.pow(2, 50),
    Ti: Math.pow(2, 40),
    Gi: Math.pow(2, 30),
    Mi: Math.pow(2, 20),
    ki: Math.pow(2, 10),
};
const TRANSLATED_CATEGORIES = {
    weight: _t("Weight"),
    distance: _t("Distance"),
    time: _t("Time"),
    pressure: _t("Pressure"),
    force: _t("Force"),
    energy: _t("Energy"),
    power: _t("Power"),
    magnetism: _t("Magnetism"),
    temperature: _t("Temperature"),
    volume: _t("Volume"),
    area: _t("Area"),
    information: _t("Information"),
    speed: _t("Speed"),
};
const UNIT_OPTIONS = Object.entries(UNITS).map(([key, value]) => ({
    value: key,
    label: getTranslatedCategory(value.category),
}));
function getTranslatedCategory(key) {
    return TRANSLATED_CATEGORIES[key] ?? "";
}
function getTransformation(key) {
    for (const [prefix, value] of Object.entries(UNIT_PREFIXES)) {
        if (prefix && !key.startsWith(prefix))
            continue;
        const _key = key.slice(prefix.length);
        let conversion = UNITS[_key];
        if (!conversion && UNITS_ALIASES[_key]) {
            conversion = UNITS[UNITS_ALIASES[_key]];
        }
        if (conversion) {
            return {
                ...conversion,
                factor: conversion.order ? Math.pow(value, conversion.order) : value,
            };
        }
    }
    return;
}

// -----------------------------------------------------------------------------
// CONVERT
// -----------------------------------------------------------------------------
const CONVERT = {
    description: _t("Converts a numeric value to a different unit of measure."),
    args: [
        arg("value (number)", _t("the numeric value in start_unit to convert to end_unit")),
        arg("start_unit (string)", _t("The starting unit, the unit currently assigned to value"), UNIT_OPTIONS),
        arg("end_unit (string)", _t("The unit of measure into which to convert value"), UNIT_OPTIONS),
    ],
    compute: function (value, startUnit, endUnit) {
        const _value = toNumber(value, this.locale);
        const _startUnit = toString(startUnit);
        const _endUnit = toString(endUnit);
        const startConversion = getTransformation(_startUnit);
        const endConversion = getTransformation(_endUnit);
        if (!startConversion) {
            return {
                value: CellErrorType.GenericError,
                message: _t("Invalid units of measure ('%s')", _startUnit),
            };
        }
        if (!endConversion) {
            return {
                value: CellErrorType.GenericError,
                message: _t("Invalid units of measure ('%s')", _endUnit),
            };
        }
        if (startConversion.category !== endConversion.category) {
            return {
                value: CellErrorType.GenericError,
                message: _t("Incompatible units of measure ('%s' vs '%s')", getTranslatedCategory(startConversion.category), getTranslatedCategory(endConversion.category)),
            };
        }
        return {
            value: endConversion.inverseTransform(startConversion.factor * startConversion.transform(_value)) /
                endConversion.factor,
            format: value?.format,
        };
    },
    isExported: true,
};

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

const DEFAULT_STARTING_AT = 1;
/** Regex matching all the words in a string */
const wordRegex = /[A-Za-zÀ-ÖØ-öø-ÿ]+/g;
const MATCH_MODE_OPTIONS = [
    { value: 0, label: _t("Case-sensitive (default)") },
    { value: 1, label: _t("Case-insensitive") },
];
const MATCH_END_OPTIONS = [
    { value: 0, label: _t("Don't match to end (default)") },
    { value: 1, label: _t("Match to end") },
];
// -----------------------------------------------------------------------------
// CHAR
// -----------------------------------------------------------------------------
const CHAR = {
    description: _t("Gets character associated with number."),
    args: [
        arg("table_number (number)", _t("The number of the character to look up from the current Unicode table in decimal format.")),
    ],
    compute: function (tableNumber) {
        const _tableNumber = Math.trunc(toNumber(tableNumber, this.locale));
        if (_tableNumber < 1) {
            return new EvaluationError(_t("The table_number (%s) is out of range.", _tableNumber));
        }
        return String.fromCharCode(_tableNumber);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// CLEAN
// -----------------------------------------------------------------------------
const CLEAN = {
    description: _t("Remove non-printable characters from a piece of text."),
    args: [arg("text (string)", _t("The text whose non-printable characters are to be removed."))],
    compute: function (text) {
        const _text = toString(text);
        let cleanedStr = "";
        for (const char of _text) {
            if (char && char.charCodeAt(0) > 31) {
                cleanedStr += char;
            }
        }
        return cleanedStr;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// CONCATENATE
// -----------------------------------------------------------------------------
const CONCATENATE = {
    description: _t("Appends strings to one another."),
    args: [
        arg("string1 (string, range<string>)", _t("The initial string.")),
        arg("string2 (string, range<string>, repeating)", _t("More strings to append in sequence.")),
    ],
    compute: function (...datas) {
        return reduceAny(datas, (acc, a) => acc + toString(a), "");
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// EXACT
// -----------------------------------------------------------------------------
const EXACT = {
    description: _t("Tests whether two strings are identical."),
    args: [
        arg("string1 (string)", _t("The first string to compare.")),
        arg("string2 (string)", _t("The second string to compare.")),
    ],
    compute: function (string1, string2) {
        return toString(string1) === toString(string2);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// FIND
// -----------------------------------------------------------------------------
const FIND = {
    description: _t("First position of string found in text, case-sensitive."),
    args: [
        arg("search_for (string)", _t("The string to look for within text_to_search.")),
        arg("text_to_search (string)", _t("The text to search for the first occurrence of search_for.")),
        arg(`starting_at (number, default=${DEFAULT_STARTING_AT})`, _t("The character within text_to_search at which to start the search.")),
    ],
    compute: function (searchFor, textToSearch, startingAt = { value: DEFAULT_STARTING_AT }) {
        const _searchFor = toString(searchFor);
        const _textToSearch = toString(textToSearch);
        const _startingAt = toNumber(startingAt, this.locale);
        if (_textToSearch === "") {
            return new EvaluationError(_t("The text_to_search must be non-empty."));
        }
        if (_startingAt < 1) {
            return new EvaluationError(_t("The starting_at (%s) must be greater than or equal to 1.", _startingAt));
        }
        const result = _textToSearch.indexOf(_searchFor, _startingAt - 1);
        if (result === -1) {
            return new EvaluationError(_t("In [[FUNCTION_NAME]] evaluation, cannot find '%s' within '%s'.", _searchFor, _textToSearch));
        }
        return result + 1;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// JOIN
// -----------------------------------------------------------------------------
const JOIN = {
    description: _t("Concatenates elements of arrays with delimiter."),
    args: [
        arg("delimiter (string)", _t("The character or string to place between each concatenated value.")),
        arg("value_or_array1 (string, range<string>)", _t("The value or values to be appended using delimiter.")),
        arg("value_or_array2 (string, range<string>, repeating)", _t("More values to be appended using delimiter.")),
    ],
    compute: function (delimiter, ...valuesOrArrays) {
        const _delimiter = toString(delimiter);
        return reduceAny(valuesOrArrays, (acc, a) => (acc ? acc + _delimiter : "") + toString(a), "");
    },
};
// -----------------------------------------------------------------------------
// LEFT
// -----------------------------------------------------------------------------
const LEFT = {
    description: _t("Substring from beginning of specified string."),
    args: [
        arg("text (string)", _t("The string from which the left portion will be returned.")),
        arg("number_of_characters (number, optional)", _t("The number of characters to return from the left side of string.")),
    ],
    compute: function (text, ...args) {
        const _numberOfCharacters = args.length ? toNumber(args[0], this.locale) : 1;
        if (_numberOfCharacters < 0) {
            return new EvaluationError(_t("The number_of_characters (%s) must be positive or null.", _numberOfCharacters));
        }
        return toString(text).substring(0, _numberOfCharacters);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// LEN
// -----------------------------------------------------------------------------
const LEN = {
    description: _t("Length of a string."),
    args: [arg("text (string)", _t("The string whose length will be returned."))],
    compute: function (text) {
        return toString(text).length;
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// LOWER
// -----------------------------------------------------------------------------
const LOWER = {
    description: _t("Converts a specified string to lowercase."),
    args: [arg("text (string)", _t("The string to convert to lowercase."))],
    compute: function (text) {
        return toString(text).toLowerCase();
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// MID
// -----------------------------------------------------------------------------
const MID = {
    description: _t("A segment of a string."),
    args: [
        arg("text (string)", _t("The string to extract a segment from.")),
        arg("starting_at (number)", _t("The index from the left of string from which to begin extracting. The first character in string has the index 1.")),
        arg("extract_length (number)", _t("The length of the segment to extract.")),
    ],
    compute: function (text, starting_at, extract_length) {
        const _text = toString(text);
        const _starting_at = toNumber(starting_at, this.locale);
        const _extract_length = toNumber(extract_length, this.locale);
        if (_starting_at < 1) {
            return new EvaluationError(_t("The starting_at argument (%s) must be positive greater than one.", _starting_at.toString()));
        }
        if (_extract_length < 0) {
            return new EvaluationError(_t("The extract_length argument (%s) must be positive or null.", _extract_length));
        }
        return _text.slice(_starting_at - 1, _starting_at + _extract_length - 1);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// PROPER
// -----------------------------------------------------------------------------
const PROPER = {
    description: _t("Capitalizes each word in a specified string."),
    args: [
        arg("text_to_capitalize (string)", _t("The text which will be returned with the first letter of each word in uppercase and all other letters in lowercase.")),
    ],
    compute: function (text) {
        const _text = toString(text);
        return _text.replace(wordRegex, (word) => {
            return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
        });
    },
    isExported: true,
};
const REGEXEXTRACT_DEFAULT_MODE = 0;
const REGEXEXTRACT_DEFAULT_CASE_SENSITIVITY = 0;
// -----------------------------------------------------------------------------
// REGEXEXTRACT
// -----------------------------------------------------------------------------
const REGEXEXTRACT = {
    description: _t("Extract text from a string based on the supplied regular expression."),
    args: [
        arg("text (string)", _t("The string on which you want to extract text.")),
        arg("pattern (string)", _t("The regular expression pattern to match against the text.")),
        arg(`return_mode (number, default=${REGEXEXTRACT_DEFAULT_MODE})`, _t("0 = first match, 1 = all matches as an array, 2 = capturing groups from the first match as an array."), [
            { value: 0, label: _t("First match") },
            { value: 1, label: _t("All matches") },
            { value: 2, label: _t("Capture groups of first match") },
        ]),
        arg(`case_sensitivity (number, default=${REGEXEXTRACT_DEFAULT_CASE_SENSITIVITY})`, _t("Whether the match is case-sensitive."), [
            { value: 0, label: _t("Case-sensitive") },
            { value: 1, label: _t("Case-insensitive") },
        ]),
    ],
    compute: function (text, pattern, return_mode = { value: REGEXEXTRACT_DEFAULT_MODE }, newText = { value: REGEXEXTRACT_DEFAULT_CASE_SENSITIVITY }) {
        const _text = toString(text);
        const _pattern = toString(pattern);
        const _returnMode = toNumber(return_mode, this.locale);
        const _caseSensitivity = toNumber(newText, this.locale);
        if (_text === "" || _pattern === "") {
            return { value: "" };
        }
        if (_returnMode < 0 || _returnMode > 2) {
            return new EvaluationError(_t("The return_mode (%s) must be 0, 1 or 2.", _returnMode));
        }
        if (_caseSensitivity !== 0 && _caseSensitivity !== 1) {
            return new EvaluationError(_t("The case_sensitivity (%s) must be 0 or 1.", _caseSensitivity));
        }
        const flags = _caseSensitivity === 1 ? "gi" : "g";
        let regex;
        try {
            regex = new RegExp(_pattern, flags);
        }
        catch (e) {
            return new EvaluationError(_t("Invalid regular expression"));
        }
        const matches = [..._text.matchAll(regex)];
        if (matches.length === 0) {
            return { value: CellErrorType.NotAvailable, message: _t("No matches found.") };
        }
        if (_returnMode === 0) {
            return matches[0][0];
        }
        else if (_returnMode === 1) {
            return matches.map((match) => [match[0]]);
        }
        else {
            if (matches[0].length < 2) {
                return new EvaluationError(_t("No capturing groups found."));
            }
            return matches[0].slice(1).map((s) => [s]);
        }
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// REPLACE
// -----------------------------------------------------------------------------
const REPLACE = {
    description: _t("Replaces part of a text string with different text."),
    args: [
        arg("text (string)", _t("The text, a part of which will be replaced.")),
        arg("position (number)", _t("The position where the replacement will begin (starting from 1).")),
        arg("length (number)", _t("The number of characters in the text to be replaced.")),
        arg("new_text (string)", _t("The text which will be inserted into the original text.")),
    ],
    compute: function (text, position, length, newText) {
        const _position = toNumber(position, this.locale);
        if (_position < 1) {
            return new EvaluationError(_t("The position (%s) must be greater than or equal to 1.", _position));
        }
        const _text = toString(text);
        const _length = toNumber(length, this.locale);
        const _newText = toString(newText);
        return _text.substring(0, _position - 1) + _newText + _text.substring(_position - 1 + _length);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// RIGHT
// -----------------------------------------------------------------------------
const RIGHT = {
    description: _t("A substring from the end of a specified string."),
    args: [
        arg("text (string)", _t("The string from which the right portion will be returned.")),
        arg("number_of_characters (number, optional)", _t("The number of characters to return from the right side of string.")),
    ],
    compute: function (text, ...args) {
        const _numberOfCharacters = args.length ? toNumber(args[0], this.locale) : 1;
        if (_numberOfCharacters < 0) {
            return new EvaluationError(_t("The number_of_characters (%s) must be positive or null.", _numberOfCharacters));
        }
        const _text = toString(text);
        const stringLength = _text.length;
        return _text.substring(stringLength - _numberOfCharacters, stringLength);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// SEARCH
// -----------------------------------------------------------------------------
const SEARCH = {
    description: _t("First position of string found in text, ignoring case."),
    args: [
        arg("search_for (string)", _t("The string to look for within text_to_search.")),
        arg("text_to_search (string)", _t("The text to search for the first occurrence of search_for.")),
        arg(`starting_at (number, default=${DEFAULT_STARTING_AT})`, _t("The character within text_to_search at which to start the search.")),
    ],
    compute: function (searchFor, textToSearch, startingAt = { value: DEFAULT_STARTING_AT }) {
        const _searchFor = toString(searchFor).toLowerCase();
        const _textToSearch = toString(textToSearch).toLowerCase();
        const _startingAt = toNumber(startingAt, this.locale);
        if (_textToSearch === "") {
            return {
                value: CellErrorType.GenericError,
                message: _t("The text_to_search must be non-empty."),
            };
        }
        if (_startingAt < 1) {
            return {
                value: CellErrorType.GenericError,
                message: _t("The starting_at (%s) must be greater than or equal to 1.", _startingAt),
            };
        }
        const result = _textToSearch.indexOf(_searchFor, _startingAt - 1);
        if (result === -1) {
            return {
                value: CellErrorType.GenericError,
                message: _t("In [[FUNCTION_NAME]] evaluation, cannot find '%s' within '%s'.", _searchFor, _textToSearch),
            };
        }
        return { value: result + 1 };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// SPLIT
// -----------------------------------------------------------------------------
const SPLIT_DEFAULT_SPLIT_BY_EACH = true;
const SPLIT_DEFAULT_REMOVE_EMPTY_TEXT = true;
const SPLIT = {
    description: _t("Split text by specific character delimiter(s)."),
    args: [
        arg("text (string)", _t("The text to divide.")),
        arg("delimiter (string)", _t("The character or characters to use to split text.")),
        arg(`split_by_each (boolean, default=${SPLIT_DEFAULT_SPLIT_BY_EACH}})`, _t("Whether or not to divide text around each character contained in delimiter.")),
        arg(`remove_empty_text (boolean, default=${SPLIT_DEFAULT_REMOVE_EMPTY_TEXT})`, _t("Whether or not to remove empty text messages from the split results. The default behavior is to treat \
        consecutive delimiters as one (if TRUE). If FALSE, empty cells values are added between consecutive delimiters.")),
    ],
    compute: function (text, delimiter, splitByEach = { value: SPLIT_DEFAULT_SPLIT_BY_EACH }, removeEmptyText = { value: SPLIT_DEFAULT_REMOVE_EMPTY_TEXT }) {
        const _text = toString(text);
        const _delimiter = escapeRegExp(toString(delimiter));
        const _splitByEach = toBoolean(splitByEach);
        const _removeEmptyText = toBoolean(removeEmptyText);
        if (_delimiter.length <= 0) {
            return new EvaluationError(_t("The delimiter (%s) must be not be empty.", _delimiter));
        }
        const regex = _splitByEach ? new RegExp(`[${_delimiter}]`, "g") : new RegExp(_delimiter, "g");
        let result = _text.split(regex);
        if (_removeEmptyText) {
            result = result.filter((text) => text !== "");
        }
        return transposeMatrix([result]);
    },
    isExported: false,
};
// -----------------------------------------------------------------------------
// SUBSTITUTE
// -----------------------------------------------------------------------------
const SUBSTITUTE = {
    description: _t("Replaces existing text with new text in a string."),
    args: [
        arg("text_to_search (string)", _t("The text within which to search and replace.")),
        arg("search_for (string)", _t("The string to search for within text_to_search.")),
        arg("replace_with (string)", _t("The string that will replace search_for.")),
        arg("occurrence_number (number, optional)", _t("The instance of search_for within text_to_search to replace with replace_with. By default, all occurrences of search_for are replaced; however, if occurrence_number is specified, only the indicated instance of search_for is replaced.")),
    ],
    compute: function (textToSearch, searchFor, replaceWith, occurrenceNumber) {
        const _occurrenceNumber = toNumber(occurrenceNumber, this.locale);
        if (_occurrenceNumber < 0) {
            return new EvaluationError(_t("The occurrenceNumber (%s) must be positive or null.", _occurrenceNumber));
        }
        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_DEFAULT_IGNORE_EMPTY = true;
const TEXTJOIN = {
    description: _t("Combines text from multiple strings and/or arrays."),
    args: [
        arg("delimiter (string)", _t(" A string, possible empty, or a reference to a valid string. If empty, the text will be simply concatenated.")),
        arg("ignore_empty (boolean)", _t("A boolean; if TRUE, empty cells selected in the text arguments won't be included in the result."), [
            { value: true, label: _t("Ignore empty cells") },
            { value: false, label: _t("Include empty cells (default)") },
        ]),
        arg("text1 (string, range<string>)", _t("Any text item. This could be a string, or an array of strings in a range.")),
        arg("text2 (string, range<string>, repeating)", _t("Additional text item(s).")),
    ],
    compute: function (delimiter, ignoreEmpty = { value: TEXTJOIN_DEFAULT_IGNORE_EMPTY }, ...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,
};
// -----------------------------------------------------------------------------
// TEXTSPLIT
// -----------------------------------------------------------------------------
const TEXTSPLIT_DEFAULT_IGNORE_EMPTY = false;
const TEXTSPLIT_DEFAULT_MATCH_MODE = 0;
const TEXTSPLIT = {
    description: _t("Splits text into rows or columns using specified column and row delimiters."),
    args: [
        arg("text (string)", _t("The text to split.")),
        arg("col_delimiter (string, range<string>)", _t("Character or string to split columns by.")),
        arg("row_delimiter (string, range<string>, optional)", _t("Character or string to split rows by.")),
        arg(`ignore_empty (boolean, default=${TEXTSPLIT_DEFAULT_IGNORE_EMPTY})`, _t("Whether to ignore empty cells."), [
            { value: false, label: _t("Include empty cells (default)") },
            { value: true, label: _t("Ignore empty cells") },
        ]),
        arg(`match_mode (number, default=${TEXTSPLIT_DEFAULT_MATCH_MODE})`, _t("Searches the text for a delimiter match. By default, a case-sensitive match is done."), MATCH_MODE_OPTIONS),
        arg(`pad_with (string, default="${CellErrorType.NotAvailable}")`, _t("The value to use for padding empty cells.")),
    ],
    compute: function (text, colDelimiter, rowDelimiter, ignoreEmpty = { value: TEXTSPLIT_DEFAULT_IGNORE_EMPTY }, matchMode = { value: TEXTSPLIT_DEFAULT_MATCH_MODE }, padWith = new NotAvailableError()) {
        const _text = toString(text);
        if (_text.length <= 0) {
            return new EvaluationError(_t("No text to split."));
        }
        if (colDelimiter === undefined && rowDelimiter === undefined) {
            return new EvaluationError(_t("At least one delimiter must be provided."));
        }
        const _colDelimiters = toMatrix(colDelimiter)
            .flat()
            .map((v) => escapeRegExp(toString(v)));
        const _rowDelimiters = toMatrix(rowDelimiter)
            .flat()
            .map((v) => escapeRegExp(toString(v)));
        if (_colDelimiters.some((v) => v === "") || _rowDelimiters.some((v) => v === "")) {
            return new EvaluationError(_t("The delimiters cannot be empty values."));
        }
        const _ignoreEmpty = toBoolean(ignoreEmpty);
        const _matchMode = toNumber(matchMode, this.locale);
        if (![0, 1].includes(_matchMode)) {
            return new EvaluationError(_t("match_mode should be a value of 0 or 1."));
        }
        const cells = [];
        const regexpFlags = _matchMode === 1 ? "gi" : "g";
        // only keep the row delimiters that are not in the column delimiters to prioritize spliting by columns
        const filteredRowDelimiters = _rowDelimiters.filter((delim) => !_colDelimiters.includes(delim));
        let rowParts = filteredRowDelimiters.length
            ? _text.split(new RegExp(filteredRowDelimiters.join("|"), regexpFlags))
            : [_text];
        if (_ignoreEmpty) {
            rowParts = rowParts.filter((v) => v !== "");
        }
        const colRegexp = new RegExp(_colDelimiters.join("|"), regexpFlags);
        for (const rowText of rowParts) {
            let columns = _colDelimiters.length ? rowText.split(colRegexp) : [rowText];
            if (_ignoreEmpty) {
                columns = columns.filter((v) => v !== "");
            }
            cells.push(columns.map((value) => ({ value })));
        }
        const maxLength = Math.max(...cells.map((row) => row.length));
        for (const row of cells) {
            while (row.length < maxLength) {
                row.push(padWith);
            }
        }
        return transposeMatrix(cells);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// TRIM
// -----------------------------------------------------------------------------
const TRIM = {
    description: _t("Removes space characters."),
    args: [
        arg("text (string)", _t("The text or reference to a cell containing text to be trimmed.")),
    ],
    compute: function (text) {
        return trimContent(toString(text));
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// UPPER
// -----------------------------------------------------------------------------
const UPPER = {
    description: _t("Converts a specified string to uppercase."),
    args: [arg("text (string)", _t("The string to convert to uppercase."))],
    compute: function (text) {
        return toString(text).toUpperCase();
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// TEXT
// -----------------------------------------------------------------------------
const TEXT = {
    description: _t("Converts a number to text according to a specified format."),
    args: [
        arg("number (number)", _t("The number, date or time to format.")),
        arg("format (string)", _t('The case-sensitive format of the result, enclosed in quotation marks. Examples: "0.00" rounded to 2 decimal places, "hh:mm:ss" for hour:minutes:seconds.')),
    ],
    compute: function (number, format) {
        const _number = toNumber(number, this.locale);
        return formatValue(_number, { format: toString(format), locale: this.locale });
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// VALUE
// -----------------------------------------------------------------------------
const VALUE = {
    description: _t("Converts a string to a numeric value."),
    args: [arg("value (number)", _t("the string to be converted"))],
    compute: function (value) {
        return toNumber(value, this.locale);
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// TEXTAFTER
// -----------------------------------------------------------------------------
const TEXT_FN_DEFAULT_INSTANCE = 1;
const TEXT_FN_DEFAULT_MATCH_MODE = 0;
const TEXT_FN_DEFAULT_MATCH_END = 0;
const TEXTAFTER = {
    description: _t("Returns text that occurs after a given substring or delimiter."),
    args: [
        arg("text (string)", _t("The source text.")),
        arg("delimiter (string)", _t("The substring after which text will be returned.")),
        arg(`instance_num (number, default=${TEXT_FN_DEFAULT_INSTANCE})`, _t("The desired instance of the delimiter after which we extract the text. A negative number searches from the end.")),
        arg(`match_mode (number, default=${TEXT_FN_DEFAULT_MATCH_MODE})`, _t("Searches the text for a delimiter match. By default, a case-sensitive match is done."), MATCH_MODE_OPTIONS),
        arg(`match_end (number, default=${TEXT_FN_DEFAULT_MATCH_END}))`, _t("Whether to treat the end of text as a delimiter."), MATCH_END_OPTIONS),
        arg(`if_not_found (string, default="${CellErrorType.NotAvailable}")`, _t("Value to return if the delimiter is not found.")),
    ],
    compute: function (text, delimiter, matchIndex = { value: TEXT_FN_DEFAULT_INSTANCE }, matchMode = { value: TEXT_FN_DEFAULT_MATCH_MODE }, matchEnd = { value: TEXT_FN_DEFAULT_MATCH_END }, ifNotFound = new NotAvailableError()) {
        const _text = toString(text);
        const _matchIndex = toNumber(matchIndex, this.locale);
        const _matchMode = toNumber(matchMode, this.locale);
        const _matchEnd = toNumber(matchEnd, this.locale);
        if (_matchIndex === 0) {
            return new EvaluationError(_t("The instance_num (%s) must not be zero.", _matchIndex));
        }
        if (_matchMode !== 0 && _matchMode !== 1) {
            return new EvaluationError(_t("match_mode should have a value of 0 or 1."));
        }
        if (_matchEnd !== 0 && _matchEnd !== 1) {
            return new EvaluationError(_t("match_end should have a value of 0 or 1."));
        }
        const _delimiter = toString(delimiter);
        if (_delimiter === "") {
            return Math.sign(_matchIndex) > 0 ? { value: _text } : { value: "" };
        }
        const flags = _matchMode === 1 ? "gi" : "g";
        const pattern = escapeRegExp(_delimiter);
        const regexp = new RegExp(pattern, flags);
        let matchIndices = [..._text.matchAll(regexp)].map((match) => match.index + pattern.length);
        if (_matchIndex < 0) {
            matchIndices = matchIndices.reverse();
        }
        // If _matchEnd, we act like the text is appended by the delimiter (or prepended if negative index)
        if (_matchEnd && Math.abs(_matchIndex) === matchIndices.length + 1) {
            return Math.sign(_matchIndex) > 0 ? { value: "" } : { value: _text };
        }
        const targetIndex = matchIndices[Math.abs(_matchIndex) - 1];
        return targetIndex === undefined ? ifNotFound : { value: _text.substring(targetIndex) };
    },
    isExported: true,
};
// -----------------------------------------------------------------------------
// TEXTBEFORE
// -----------------------------------------------------------------------------
const TEXTBEFORE = {
    description: _t("Returns text that occurs before a given substring or delimiter."),
    args: [
        arg("text (string)", _t("The source text.")),
        arg("delimiter (string)", _t("The substring after which text will be returned.")),
        arg(`instance_num (number, default=${TEXT_FN_DEFAULT_INSTANCE})`, _t("The desired instance of the delimiter before which we extract the text. A negative number searches from the end.")),
        arg(`match_mode (number, default=${TEXT_FN_DEFAULT_MATCH_MODE})`, _t("Searches the text for a delimiter match. By default, a case-sensitive match is done."), MATCH_MODE_OPTIONS),
        arg(`match_end (number, default=${TEXT_FN_DEFAULT_MATCH_END}))`, _t("Whether to match a delimiter against the end of the text."), MATCH_END_OPTIONS),
        arg(`if_not_found (string, default="${CellErrorType.NotAvailable}")`, _t("Value to return if the delimiter is not found.")),
    ],
    compute: function (text, delimiter, matchIndex = { value: TEXT_FN_DEFAULT_INSTANCE }, matchMode = { value: TEXT_FN_DEFAULT_MATCH_MODE }, matchEnd = { value: TEXT_FN_DEFAULT_MATCH_END }, ifNotFound = new NotAvailableError()) {
        const _text = toString(text);
        const _matchIndex = toNumber(matchIndex, this.locale);
        const _matchMode = toNumber(matchMode, this.locale);
        const _matchEnd = toNumber(matchEnd, this.locale);
        if (_matchIndex === 0) {
            return new EvaluationError(_t("The instance_num (%s) must not be zero.", _matchIndex));
        }
        if (_matchMode !== 0 && _matchMode !== 1) {
            return new EvaluationError(_t("match_mode should have a value of 0 or 1."));
        }
        if (_matchEnd !== 0 && _matchEnd !== 1) {
            return new EvaluationError(_t("match_end should have a value of 0 or 1."));
        }
        const _delimiter = toString(delimiter);
        if (_delimiter === "") {
            return Math.sign(_matchIndex) > 0 ? { value: "" } : { value: _text };
        }
        const flags = _matchMode === 1 ? "gi" : "g";
        const pattern = escapeRegExp(_delimiter);
        const regexp = new RegExp(pattern, flags);
        let matchIndices = [..._text.matchAll(regexp)].map((match) => match.index + pattern.length);
        if (_matchIndex < 0) {
            matchIndices = matchIndices.reverse();
        }
        // If _matchEnd, we act like the text is appended by the delimiter (or prepended if negative index)
        if (_matchEnd && Math.abs(_matchIndex) === matchIndices.length + 1) {
            return Math.sign(_matchIndex) > 0 ? { value: _text } : { value: "" };
        }
        const targetIndex = matchIndices[Math.abs(_matchIndex) - 1];
        return targetIndex === undefined
            ? ifNotFound
            : { value: _text.substring(0, targetIndex - _delimiter.length) };
    },
    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,
    REGEXEXTRACT: REGEXEXTRACT,
    REPLACE: REPLACE,
    RIGHT: RIGHT,
    SEARCH: SEARCH,
    SPLIT: SPLIT,
    SUBSTITUTE: SUBSTITUTE,
    TEXT: TEXT,
    TEXTAFTER: TEXTAFTER,
    TEXTBEFORE: TEXTBEFORE,
    TEXTJOIN: TEXTJOIN,
    TEXTSPLIT: TEXTSPLIT,
    TRIM: TRIM,
    UPPER: UPPER,
    VALUE: VALUE
});

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

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

const categories = [
    { name: _t("Array"), functions: array },
    { name: _t("Database"), functions: database },
    { name: _t("Date"), functions: date },
    { name: _t("Filter"), functions: filter },
    { name: _t("Financial"), functions: financial },
    { name: _t("Info"), functions: info },
    { name: _t("Lookup"), functions: lookup },
    { name: _t("Logical"), functions: logical },
    { name: _t("Math"), functions: math },
    { name: _t("Misc"), functions: misc },
    { name: _t("Operator"), functions: operators },
    { name: _t("Statistical"), functions: statistical },
    { name: _t("Text"), functions: text },
    { name: _t("Engineering"), functions: engineering },
    { name: _t("Web"), functions: web },
    { name: _t("Parser"), functions: parser },
];
const functionNameRegex = /^[A-Z0-9\_\.]+$/;
//------------------------------------------------------------------------------
// Function registry
//------------------------------------------------------------------------------
class FunctionRegistry extends Registry {
    mapping = {};
    add(name, addDescr) {
        name = name.toUpperCase();
        if (name in this.content) {
            throw new Error(`${name} is already present in this registry!`);
        }
        return this.replace(name, addDescr);
    }
    replace(name, addDescr) {
        name = name.toUpperCase();
        if (!functionNameRegex.test(name)) {
            throw new Error(_t("Invalid function name %s. Function names can exclusively contain alphanumerical values separated by dots (.) or underscore (_)", name));
        }
        const descr = addMetaInfoFromArg(name, addDescr);
        validateArguments(descr);
        this.mapping[name] = createComputeFunction(descr);
        super.replace(name, descr);
        return this;
    }
}
const functionRegistry = new FunctionRegistry();
for (const category of categories) {
    const fns = category.functions;
    for (let name in fns) {
        const addDescr = fns[name];
        addDescr.category = addDescr.category || category.name;
        name = name.replace(/_/g, ".");
        functionRegistry.add(name, { isExported: false, ...addDescr });
        createAutocompleteArgumentsProvider(name, addDescr.args);
    }
}
//------------------------------------------------------------------------------
// CREATE COMPUTE FUNCTION
//------------------------------------------------------------------------------
function createComputeFunction(descr) {
    function vectorizedCompute(...args) {
        const acceptToVectorize = [];
        const getArgToFocus = argTargeting(descr, args.length);
        //#region Compute vectorisation limits
        for (let i = 0; i < args.length; i++) {
            const argIndex = getArgToFocus(i) ?? -1;
            const argDefinition = descr.args[argIndex];
            const arg = args[i];
            if (!isMatrix(arg) && argDefinition.acceptMatrixOnly) {
                throw new BadExpressionError(_t("Function %s expects the parameter '%s' to be reference to a cell or range.", descr.name, (i + 1).toString()));
            }
            acceptToVectorize.push(!argDefinition.acceptMatrix);
        }
        return replaceErrorPlaceholderInResult(applyVectorization(errorHandlingCompute.bind(this), args, acceptToVectorize));
    }
    function replaceErrorPlaceholderInResult(result) {
        if (!isMatrix(result)) {
            replaceFunctionNamePlaceholder(result, descr.name);
        }
        else {
            matrixForEach(result, (result) => replaceFunctionNamePlaceholder(result, descr.name));
        }
        return result;
    }
    function errorHandlingCompute(...args) {
        for (let i = 0; i < args.length; i++) {
            const arg = args[i];
            const getArgToFocus = argTargeting(descr, args.length);
            const argDefinition = descr.args[getArgToFocus(i) || i];
            // Early exit if the argument is an error and the function does not accept errors
            // We only check scalar arguments, not matrix arguments for performance reasons.
            // Casting helpers are responsible for handling errors in matrix arguments.
            if (!argDefinition.acceptErrors && !isMatrix(arg) && isEvaluationError(arg?.value)) {
                return arg;
            }
        }
        try {
            return computeFunctionToObject.apply(this, args);
        }
        catch (e) {
            return handleError(e, descr.name);
        }
    }
    function computeFunctionToObject(...args) {
        if (this.debug) {
            // eslint-disable-next-line no-debugger
            debugger;
        }
        const result = descr.compute.apply(this, args);
        if (!isMatrix(result)) {
            if (typeof result === "object" && result !== null && "value" in result) {
                return result;
            }
            descr.name;
            return { value: result };
        }
        if (typeof result[0][0] === "object" && result[0][0] !== null && "value" in result[0][0]) {
            return result;
        }
        return matrixMap(result, (row) => ({ value: row }));
    }
    return vectorizedCompute;
}
function handleError(e, functionName) {
    // the error could be an user error (instance of EvaluationError)
    // or a javascript error (instance of Error)
    // we don't want block the user with an implementation error
    // so we fallback to a generic error
    if (hasStringValue(e) && isEvaluationError(e.value)) {
        if (hasStringMessage(e)) {
            replaceFunctionNamePlaceholder(e, functionName);
        }
        return e;
    }
    console.error(e);
    return new EvaluationError(implementationErrorMessage + (hasStringMessage(e) ? " " + e.message : ""));
}
function hasStringValue(obj) {
    return (obj?.value !== undefined &&
        typeof obj.value === "string");
}
function replaceFunctionNamePlaceholder(functionResult, functionName) {
    // for performance reasons: change in place and only if needed
    if (functionResult.message?.includes("[[FUNCTION_NAME]]")) {
        functionResult.message = functionResult.message.replace("[[FUNCTION_NAME]]", functionName);
    }
}
const implementationErrorMessage = _t("An unexpected error occurred. Submit a support ticket at odoo.com/help.");
function hasStringMessage(obj) {
    return (obj?.message !== undefined &&
        typeof obj.message === "string");
}
function createAutocompleteArgumentsProvider(formulaName, args) {
    for (let i = 0; i < args.length; i++) {
        const proposalValues = args[i].proposalValues;
        if (proposalValues === undefined || proposalValues.length === 0) {
            continue;
        }
        const getProposals = (tokenAtCursor) => {
            const functionContext = tokenAtCursor.functionContext;
            if (!functionContext ||
                functionContext.parent.toUpperCase() !== formulaName.toUpperCase() ||
                functionContext.argPosition !== i) {
                return;
            }
            const proposals = [];
            let text = "";
            for (const { value, label } of proposalValues) {
                switch (typeof value) {
                    case "string":
                        text = `"${value}"`;
                        break;
                    case "number":
                        text = value.toString();
                        break;
                    case "boolean":
                        text = value ? "TRUE" : "FALSE";
                        break;
                }
                proposals.push({
                    text,
                    description: label,
                    htmlContent: [
                        {
                            value: text,
                            color: typeof value === "string" ? tokenColors.STRING : tokenColors.NUMBER,
                        },
                    ],
                    fuzzySearchKey: text,
                    alwaysExpanded: true,
                });
            }
            return proposals;
        };
        autoCompleteProviders.add(`${formulaName}_function_${args[i].name}_argument_proposals`, {
            sequence: 50,
            autoSelectFirstProposal: true,
            selectProposal: insertTokenAtArgStartingPosition,
            getProposals,
        });
    }
}
/**
 * Perform the autocomplete of the composer by inserting the value
 * at the cursor position, replacing the current token if necessary.
 * Must be bound to the autocomplete provider.
 */
function insertTokenAtArgStartingPosition(tokenAtCursor, value) {
    let start = tokenAtCursor.end;
    const end = tokenAtCursor.end;
    if (!["LEFT_PAREN", "ARG_SEPARATOR"].includes(tokenAtCursor.type)) {
        // replace the whole token
        start = tokenAtCursor.start;
    }
    this.composer.stopComposerRangeSelection();
    this.composer.changeComposerCursorSelection(start, end);
    this.composer.replaceComposerCursorSelection(value);
}

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

const functions$2 = functionRegistry.content;
const OPERATOR_MAP = {
    // export for test
    "=": "EQ",
    "+": "ADD",
    "-": "MINUS",
    "*": "MULTIPLY",
    "/": "DIVIDE",
    ">=": "GTE",
    "<>": "NE",
    ">": "GT",
    "<=": "LTE",
    "<": "LT",
    "^": "POWER",
    "&": "CONCATENATE",
};
const UNARY_OPERATOR_MAP = {
    // export for test
    "-": "UMINUS",
    "+": "UPLUS",
    "%": "UNARY.PERCENT",
};
// this cache contains all compiled function code, grouped by "structure". For
// example, "=2*sum(A1:A4)" and "=2*sum(B1:B4)" are compiled into the same
// structural function.
// It is only exported for testing purposes
const functionCache = {};
// -----------------------------------------------------------------------------
// COMPILER
// -----------------------------------------------------------------------------
function compile(formula) {
    const tokens = rangeTokenize(formula);
    return compileTokens(tokens);
}
function compileTokens(tokens) {
    try {
        return compileTokensOrThrow(tokens);
    }
    catch (error) {
        return {
            tokens,
            dependencies: [],
            execute: function () {
                return error;
            },
            isBadExpression: true,
            normalizedFormula: tokens.map((t) => t.value).join(""),
        };
    }
}
function compileTokensOrThrow(tokens) {
    const { dependencies, literalValues, symbols } = formulaArguments(tokens);
    const cacheKey = compilationCacheKey(tokens);
    if (!functionCache[cacheKey]) {
        const ast = parseTokens([...tokens]);
        const scope = new Scope();
        let stringCount = 0;
        let numberCount = 0;
        let dependencyCount = 0;
        if (ast.type === "BIN_OPERATION" && ast.value === ":") {
            throw new BadExpressionError(_t("Invalid formula"));
        }
        if (ast.type === "EMPTY") {
            throw new BadExpressionError(_t("Invalid formula"));
        }
        const compiledAST = compileAST(ast);
        const code = new FunctionCodeBuilder();
        code.append(`// ${cacheKey}`);
        code.append(compiledAST);
        code.append(`return ${compiledAST.returnExpression};`);
        const 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
        "getSymbolValue", "ctx", code.toString());
        // @ts-ignore
        functionCache[cacheKey] = baseFunction;
        /**
         * This function compile the function arguments. It is mostly straightforward,
         * except that there is a non trivial transformation in one situation:
         *
         * If a function argument is asking for a range, and get a cell, we transform
         * the cell value into a range. This allow the grid model to differentiate
         * between a cell value and a non cell value.
         */
        function compileFunctionArgs(ast) {
            const { args } = ast;
            const functionName = ast.value.toUpperCase();
            const functionDefinition = functions$2[functionName];
            if (!functionDefinition) {
                throw new UnknownFunctionError(_t('Unknown function: "%s"', ast.value));
            }
            assertEnoughArgs(ast);
            const compiledArgs = [];
            const argToFocus = argTargeting(functionDefinition, args.length);
            for (let i = 0; i < args.length; i++) {
                const argDefinition = functionDefinition.args[argToFocus(i) ?? -1];
                const currentArg = args[i];
                const argTypes = argDefinition.type || [];
                // detect when an argument need to be evaluated as a meta argument
                const isMeta = argTypes.includes("META") || argTypes.includes("RANGE<META>");
                const hasRange = argTypes.some((t) => isRangeType(t));
                compiledArgs.push(compileAST(currentArg, isMeta, hasRange));
            }
            return compiledArgs;
        }
        /**
         * This function compiles all the information extracted by the parser into an
         * executable code for the evaluation of the cells content. It uses a cache to
         * not reevaluate identical code structures.
         *
         * The function is sensitive to parameter “isMeta”. This
         * parameter may vary when compiling function arguments:
         * isMeta: In some cases the function arguments expects information on the
         * cell/range other than the associated value(s). For example the COLUMN
         * function needs to receive as argument the coordinates of a cell rather
         * than its value. For this we have meta arguments.
         */
        function compileAST(ast, isMeta = false, hasRange = false) {
            const code = new FunctionCodeBuilder(scope);
            if (ast.type !== "REFERENCE" && !(ast.type === "BIN_OPERATION" && ast.value === ":")) {
                if (isMeta) {
                    throw new BadExpressionError(_t("Argument must be a reference to a cell or range."));
                }
            }
            if (ast.debug) {
                code.append("debugger;");
                code.append(`ctx["debug"] = true;`);
            }
            switch (ast.type) {
                case "BOOLEAN":
                    return code.return(`{ value: ${ast.value} }`);
                case "NUMBER":
                    return code.return(`this.literalValues.numbers[${numberCount++}]`);
                case "STRING":
                    return code.return(`this.literalValues.strings[${stringCount++}]`);
                case "REFERENCE":
                    return code.return(`${ast.value.includes(":") || hasRange ? `range` : `ref`}(deps[${dependencyCount++}], ${isMeta ? "true" : "false"})`);
                case "FUNCALL":
                    const args = compileFunctionArgs(ast).map((arg) => arg.assignResultToVariable());
                    code.append(...args);
                    const fnName = ast.value.toUpperCase();
                    return code.return(`ctx['${fnName}'](${args.map((arg) => arg.returnExpression)})`);
                case "UNARY_OPERATION": {
                    const fnName = UNARY_OPERATOR_MAP[ast.value];
                    const operand = compileAST(ast.operand, false, false).assignResultToVariable();
                    code.append(operand);
                    return code.return(`ctx['${fnName}'](${operand.returnExpression})`);
                }
                case "BIN_OPERATION": {
                    const fnName = OPERATOR_MAP[ast.value];
                    const left = compileAST(ast.left, false, false).assignResultToVariable();
                    const right = compileAST(ast.right, false, false).assignResultToVariable();
                    code.append(left);
                    code.append(right);
                    return code.return(`ctx['${fnName}'](${left.returnExpression}, ${right.returnExpression})`);
                }
                case "SYMBOL":
                    const symbolIndex = symbols.indexOf(ast.value);
                    return code.return(`getSymbolValue(this.symbols[${symbolIndex}])`);
                case "EMPTY":
                    return code.return("undefined");
            }
        }
    }
    const compiledFormula = {
        execute: functionCache[cacheKey],
        dependencies,
        literalValues,
        symbols,
        tokens,
        isBadExpression: false,
        normalizedFormula: cacheKey,
    };
    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 `=|C|+|C|+SUM(|N|,|N|,|S|)`
 */
function compilationCacheKey(tokens) {
    let cacheKey = "";
    for (const token of tokens) {
        switch (token.type) {
            case "STRING":
                cacheKey += "|S|";
                break;
            case "NUMBER":
                cacheKey += "|N|";
                break;
            case "REFERENCE":
            case "INVALID_REFERENCE":
                if (token.value.includes(":")) {
                    cacheKey += "|R|";
                }
                else {
                    cacheKey += "|C|";
                }
                break;
            case "SPACE":
                // ignore spaces
                break;
            default:
                cacheKey += token.value;
                break;
        }
    }
    return cacheKey;
}
/**
 * Return formula arguments which are references, strings and numbers.
 */
function formulaArguments(tokens) {
    const literalValues = {
        numbers: [],
        strings: [],
    };
    const dependencies = [];
    const symbols = [];
    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);
                literalValues.strings.push({ value });
                break;
            case "NUMBER": {
                const value = parseNumber(token.value, DEFAULT_LOCALE);
                literalValues.numbers.push({ value });
                break;
            }
            case "SYMBOL": {
                // function name symbols are also included here
                symbols.push(unquote(token.value, "'"));
            }
        }
    }
    return {
        dependencies,
        literalValues,
        symbols,
    };
}
/**
 * Check if arguments are supplied in the correct quantities
 */
function assertEnoughArgs(ast) {
    const nbrArgSupplied = ast.args.length;
    const functionName = ast.value.toUpperCase();
    const functionDefinition = functions$2[functionName];
    const { nbrArgRepeating, minArgRequired } = functionDefinition;
    if (nbrArgSupplied < minArgRequired) {
        throw new BadExpressionError(_t("Invalid number of arguments for the %(functionName)s function. Expected %(minArgRequired)s minimum, but got %(nbrArgSupplied)s instead.", {
            functionName,
            minArgRequired,
            nbrArgSupplied,
        }));
    }
    if (nbrArgSupplied > functionDefinition.maxArgPossible) {
        throw new BadExpressionError(_t("Invalid number of arguments for the %(functionName)s function. Expected %(maxArgPossible)s maximum, but got %(nbrArgSupplied)s instead.", {
            functionName,
            maxArgPossible: functionDefinition.maxArgPossible,
            nbrArgSupplied,
        }));
    }
    if (nbrArgRepeating > 1) {
        const nbrValueRepeating = nbrArgRepeating * Math.floor((nbrArgSupplied - minArgRequired) / nbrArgRepeating);
        const nbrValueRemaining = nbrArgSupplied - minArgRequired - nbrValueRepeating - functionDefinition.nbrArgOptional;
        if (nbrValueRemaining > 0) {
            throw new BadExpressionError(_t("Invalid number of arguments for the %(functionName)s function. Repeatable arguments should be supplied in groups of %(nbrArgRepeating)s, with up to %(nbrArgOptional)s optional. Got %(nbrValueRemaining)s too many.", {
                functionName,
                nbrArgRepeating,
                nbrArgOptional: functionDefinition.nbrArgOptional,
                nbrValueRemaining,
            }));
        }
    }
}
function isRangeType(type) {
    return type.startsWith("RANGE");
}

const functions$1 = functionRegistry.content;
function isExportableToExcel(tokens) {
    try {
        const nonExportableFunctions = iterateAstNodes(parseTokens(tokens)).filter((ast) => ast.type === "FUNCALL" && !functions$1[ast.value.toUpperCase()]?.isExported);
        return nonExportableFunctions.length === 0;
    }
    catch (error) {
        return false;
    }
}
function getFunctionsFromTokens(tokens, functionNames) {
    // Parsing is an expensive operation, so we first check if the
    // formula contains one of the function names
    if (!tokens.some((t) => t.type === "SYMBOL" && functionNames.includes(t.value.toUpperCase()))) {
        return [];
    }
    let ast;
    try {
        ast = parseTokens(tokens);
    }
    catch {
        return [];
    }
    return getFunctionsFromAST(ast, functionNames);
}
function getFunctionsFromAST(ast, functionNames) {
    return iterateAstNodes(ast)
        .filter((node) => node.type === "FUNCALL" && functionNames.includes(node.value.toUpperCase()))
        .map((node) => ({
        functionName: node.value.toUpperCase(),
        args: node.args,
    }));
}

function adaptFormulaStringRanges(defaultSheetId, formula, applyChange) {
    if (!formula.startsWith("=")) {
        return formula;
    }
    const tokens = rangeTokenize(formula);
    for (let tokenIdx = 1; tokenIdx < tokens.length; tokenIdx++) {
        if (tokens[tokenIdx].type !== "REFERENCE") {
            continue;
        }
        const sheetXC = tokens[tokenIdx].value;
        const newSheetXC = adaptStringRange(defaultSheetId, sheetXC, applyChange).range;
        if (sheetXC !== newSheetXC) {
            tokens[tokenIdx] = {
                value: newSheetXC,
                type: "REFERENCE",
            };
        }
    }
    return concat$1(tokens.map((token) => token.value));
}
function adaptStringRange(defaultSheetId, sheetXC, rangeAdapter) {
    const sheetName = splitReference(sheetXC).sheetName;
    if (sheetName
        ? !isSheetNameEqual(sheetName, rangeAdapter.sheetName.old)
        : defaultSheetId !== rangeAdapter.sheetId) {
        return { changeType: "NONE", range: sheetXC };
    }
    const sheetId = sheetName ? rangeAdapter.sheetId : defaultSheetId;
    const range = getRange(sheetXC, sheetId);
    if (range.invalidXc) {
        return { changeType: "NONE", range: sheetXC };
    }
    const change = rangeAdapter.applyChange(range);
    if (change.changeType === "NONE" || change.changeType === "REMOVE") {
        return { changeType: change.changeType, range: sheetXC };
    }
    const rangeStr = getRangeString(change.range, defaultSheetId, getSheetNameGetter(rangeAdapter));
    if (rangeStr === CellErrorType.InvalidReference) {
        return { changeType: "REMOVE", range: rangeStr };
    }
    return {
        changeType: change.changeType,
        range: rangeStr,
    };
}
function getSheetNameGetter(applyChange) {
    return (sheetId) => {
        return sheetId === applyChange.sheetId ? applyChange.sheetName.current : "";
    };
}
function defaultGetSheetSize(sheetId) {
    return { numberOfRows: Number.MAX_SAFE_INTEGER, numberOfCols: Number.MAX_SAFE_INTEGER };
}
function getRange(sheetXC, sheetId) {
    if (!rangeReference.test(sheetXC)) {
        return createInvalidRange(sheetXC);
    }
    return createRangeFromXc({ xc: sheetXC, sheetId }, defaultGetSheetSize);
}

const TREND_LINE_XAXIS_ID = "x1";
const MOVING_AVERAGE_TREND_LINE_XAXIS_ID = "xMovingAverage";
const SPREADSHEET_TO_EXCEL_TRENDLINE_TYPE_MAPPING = {
    exponential: "exp",
    logarithmic: "log",
    polynomial: "poly",
    trailingMovingAverage: "movingAvg",
};
/**
 * 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 (const 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) === CellErrorType.InvalidReference) {
            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);
    return {
        isStale,
        dataSets,
        labelRange,
    };
}
/**
 * Duplicate the dataSets. All ranges on sheetIdFrom are adapted to target
 * sheetIdTo.
 */
function duplicateDataSetsInDuplicatedSheet(sheetIdFrom, sheetIdTo, dataSets) {
    return dataSets.map((ds) => {
        return {
            dataRange: duplicateRangeInDuplicatedSheet(sheetIdFrom, sheetIdTo, ds.dataRange),
            labelCell: ds.labelCell
                ? duplicateRangeInDuplicatedSheet(sheetIdFrom, sheetIdTo, ds.labelCell)
                : undefined,
        };
    });
}
/**
 * Duplicate a range. If the range is on the sheetIdFrom, the range will target
 * sheetIdTo.
 */
function duplicateLabelRangeInDuplicatedSheet(sheetIdFrom, sheetIdTo, range) {
    return range ? duplicateRangeInDuplicatedSheet(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 "REMOVE":
            return undefined;
        default:
            return change.range;
    }
}
/**
 * Create the dataSet objects from xcs
 */
function createDataSets(getters, customizedDataSets, sheetId, dataSetsHaveTitle) {
    const dataSets = [];
    for (const dataSet of customizedDataSets) {
        const dataRange = getters.getRangeFromSheetXC(sheetId, dataSet.dataRange);
        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),
                    backgroundColor: dataSet.backgroundColor,
                    rightYAxis: dataSet.yAxisId === "y1",
                    customLabel: dataSet.label,
                    trend: dataSet.trend,
                });
            }
        }
        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),
                backgroundColor: dataSet.backgroundColor,
                rightYAxis: dataSet.yAxisId === "y1",
                customLabel: dataSet.label,
                trend: dataSet.trend,
            });
        }
    }
    return dataSets;
}
function createDataSet(getters, sheetId, fullZone, titleZone) {
    if (fullZone.left !== fullZone.right && fullZone.top !== fullZone.bottom) {
        throw new Error(`Zone should be a single column or row: ${zoneToXc(fullZone)}`);
    }
    if (titleZone) {
        const dataXC = zoneToXc(fullZone);
        const labelCellXC = zoneToXc(titleZone);
        return {
            labelCell: getters.getRangeFromSheetXC(sheetId, labelCellXC),
            dataRange: getters.getRangeFromSheetXC(sheetId, dataXC),
        };
    }
    else {
        return {
            labelCell: undefined,
            dataRange: getters.getRangeFromSheetXC(sheetId, zoneToXc(fullZone)),
        };
    }
}
/**
 * Transform a dataSet to a ExcelDataSet
 */
function toExcelDataset(getters, ds) {
    const labelZone = ds.labelCell?.zone;
    let dataZone = ds.dataRange.zone;
    if (labelZone) {
        const { numberOfRows, numberOfCols } = zoneToDimension(dataZone);
        if (numberOfRows === 1) {
            dataZone = { ...dataZone, left: dataZone.left + 1 };
        }
        else if (numberOfCols === 1) {
            dataZone = { ...dataZone, top: dataZone.top + 1 };
        }
    }
    const dataRange = createRange({ ...ds.dataRange, zone: dataZone }, getters.getSheetSize);
    let label = {};
    if (ds.customLabel) {
        label = {
            text: ds.customLabel,
        };
    }
    else if (ds.labelCell) {
        label = {
            reference: getters.getRangeString(ds.labelCell, "forceSheetReference", {
                useBoundedReference: true,
            }),
        };
    }
    let trend;
    if (ds.trend?.type) {
        trend = {
            type: ds.trend.type === "polynomial" && ds.trend.order === 1
                ? "linear"
                : SPREADSHEET_TO_EXCEL_TRENDLINE_TYPE_MAPPING[ds.trend.type],
            color: ds.trend.color,
            order: ds.trend.order ? Math.min(ds.trend.order, MAX_XLSX_POLYNOMIAL_DEGREE) : undefined,
            window: ds.trend.window || DEFAULT_WINDOW_SIZE,
        };
    }
    return {
        label,
        range: getters.getRangeString(dataRange, "forceSheetReference", { useBoundedReference: true }),
        backgroundColor: ds.backgroundColor,
        rightYAxis: ds.rightYAxis,
        trend,
    };
}
function toExcelLabelRange(getters, labelRange, shouldRemoveFirstLabel) {
    if (!labelRange)
        return undefined;
    const zone = {
        ...labelRange.zone,
    };
    if (shouldRemoveFirstLabel && labelRange.zone.bottom > labelRange.zone.top) {
        zone.top = zone.top + 1;
    }
    const range = createRange({ ...labelRange, zone: zone }, getters.getSheetSize);
    return getters.getRangeString(range, "forceSheetReference", { useBoundedReference: true });
}
/**
 * Transform a chart definition which supports dataSets (dataSets and LabelRange)
 * with an executed command
 */
function transformChartDefinitionWithDataSetsWithZone(chartSheetId, definition, applyChange) {
    let labelRange;
    if (definition.labelRange) {
        const { changeType, range: adaptedRange } = adaptStringRange(chartSheetId, definition.labelRange, applyChange);
        if (changeType !== "REMOVE") {
            labelRange = adaptedRange;
        }
    }
    const dataSets = [];
    for (const dataSet of definition.dataSets) {
        const newDataSet = { ...dataSet };
        const { changeType, range: adaptedRange } = adaptStringRange(chartSheetId, dataSet.dataRange, applyChange);
        if (changeType !== "REMOVE") {
            newDataSet.dataRange = adaptedRange;
            dataSets.push(newDataSet);
        }
    }
    return {
        ...definition,
        dataSets,
        labelRange,
    };
}
/**
 * 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 chartMutedFontColor(backgroundColor) {
    if (!backgroundColor) {
        return "#666666";
    }
    return relativeLuminance(backgroundColor) < 0.3 ? "#C8C8C8" : "#666666";
}
function checkDataset(definition) {
    if (definition.dataSets) {
        const invalidRanges = definition.dataSets.find((range) => !rangeReference.test(range.dataRange)) !== undefined;
        if (invalidRanges) {
            return "InvalidDataSet" /* CommandResult.InvalidDataSet */;
        }
        const zones = definition.dataSets.map((ds) => toUnboundedZone(ds.dataRange));
        if (zones.some((zone) => zone.top !== zone.bottom && isFullRow(zone))) {
            return "InvalidDataSet" /* CommandResult.InvalidDataSet */;
        }
    }
    return "Success" /* CommandResult.Success */;
}
function checkLabelRange(definition) {
    if (definition.labelRange) {
        const invalidLabels = !rangeReference.test(definition.labelRange || "");
        if (invalidLabels) {
            return "InvalidLabelRange" /* CommandResult.InvalidLabelRange */;
        }
    }
    return "Success" /* CommandResult.Success */;
}
function shouldRemoveFirstLabel(labelRange, dataset, dataSetsHaveTitle) {
    if (!dataSetsHaveTitle)
        return false;
    if (!labelRange)
        return false;
    if (!dataset)
        return true;
    const datasetLength = getZoneArea(dataset.dataRange.zone);
    const labelLength = getZoneArea(labelRange.zone);
    if (labelLength < datasetLength) {
        return false;
    }
    return true;
}
function getChartPositionAtCenterOfViewport(getters, chartSize) {
    const { x, y } = getters.getMainViewportCoordinates();
    const { scrollX, scrollY } = getters.getActiveSheetScrollInfo();
    const { width, height } = getters.getVisibleRect(getters.getActiveMainViewport());
    return {
        x: x + scrollX + Math.max(0, (width - chartSize.width) / 2),
        y: y + scrollY + Math.max(0, (height - chartSize.height) / 2),
    }; // Position at the center of the scrollable viewport
}
function getDefinedAxis(definition) {
    let useLeftAxis = false, useRightAxis = false;
    if ("horizontal" in definition && definition.horizontal) {
        return { useLeftAxis: true, useRightAxis: false };
    }
    for (const design of definition.dataSets || []) {
        if (design.yAxisId === "y1") {
            useRightAxis = true;
        }
        else {
            useLeftAxis = true;
        }
    }
    useLeftAxis ||= !useRightAxis;
    return { useLeftAxis, useRightAxis };
}
function formatChartDatasetValue(axisFormats, locale, humanizeNumbers = false) {
    return (value, axisId) => {
        const format = axisFormats?.[axisId];
        return formatTickValue({ format, locale }, humanizeNumbers)(value);
    };
}
function formatTickValue(localeFormat, humanizeNumbers = false) {
    return (value) => {
        value = Number(value);
        if (isNaN(value))
            return value;
        const { locale, format } = localeFormat;
        const formattedValue = humanizeNumbers
            ? humanizeNumber({ value, format }, locale)
            : formatValue(value, {
                locale,
                format: !format && Math.abs(value) >= 1000 ? "#,##" : format,
            });
        return truncateLabel(formattedValue);
    };
}
const CHART_AXIS_CHOICES = [
    { value: "left", label: _t("Left") },
    { value: "right", label: _t("Right") },
];
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 truncateLabel(label, maxLen = MAX_CHAR_LABEL) {
    if (!label) {
        return "";
    }
    if (label.length > maxLen) {
        return label.substring(0, maxLen) + "…";
    }
    return label;
}
function isTrendLineAxis(axisID) {
    return axisID === TREND_LINE_XAXIS_ID || axisID === MOVING_AVERAGE_TREND_LINE_XAXIS_ID;
}

/** This is a chartJS plugin that will draw the values of each data next to the point/bar/pie slice */
const chartShowValuesPlugin = {
    id: "chartShowValuesPlugin",
    afterDatasetsDraw(chart, args, options) {
        if (!options.showValues) {
            return;
        }
        const drawData = chart._metasets?.[0]?.data;
        if (!drawData) {
            return;
        }
        const ctx = chart.ctx;
        ctx.save();
        const { left, top, height, width } = chart.chartArea;
        ctx.beginPath();
        ctx.rect(left, top, width, height);
        ctx.clip();
        ctx.textAlign = "center";
        ctx.textBaseline = "middle";
        ctx.miterLimit = 1; // Avoid sharp artifacts on strokeText
        switch (chart.config.type) {
            case "pie":
            case "doughnut":
                drawPieChartValues(chart, options, ctx);
                break;
            case "bar":
            case "line":
            case "radar":
                options.horizontal
                    ? drawHorizontalBarChartValues(chart, options, ctx)
                    : drawLineOrBarOrRadarChartValues(chart, options, ctx);
                break;
            case "funnel":
                drawHorizontalBarChartValues(chart, options, ctx);
                break;
        }
        ctx.restore();
    },
};
function drawTextWithBackground(text, x, y, ctx) {
    ctx.lineWidth = 3; // Stroke the text with a big lineWidth width to have some kind of background
    ctx.strokeText(text, x, y);
    ctx.lineWidth = 1;
    ctx.fillText(text, x, y);
}
function drawLineOrBarOrRadarChartValues(chart, options, ctx) {
    const yMax = chart.chartArea.bottom;
    const yMin = chart.chartArea.top;
    const textsPositions = {};
    for (const dataset of chart._metasets) {
        if (isTrendLineAxis(dataset.xAxisID) || dataset.hidden) {
            continue;
        }
        const yAxisScale = chart.scales[dataset.yAxisID];
        for (let i = 0; i < dataset._parsed.length; i++) {
            const parsedValue = dataset._parsed[i];
            const value = Number(chart.config.type === "radar" ? parsedValue.r : parsedValue.y);
            if (isNaN(value)) {
                continue;
            }
            const point = dataset.data[i];
            const xPosition = point.x;
            let yPosition = 0;
            if (chart.config.type === "line" || chart.config.type === "radar") {
                yPosition = value < 0 ? point.y + 10 : point.y - 10;
            }
            else {
                const yZeroLine = yAxisScale.getPixelForValue(0);
                const distanceFromAxisOrigin = Math.abs(yZeroLine - point.y);
                const textHeight = 12; // ChartJS default text height
                if (distanceFromAxisOrigin < textHeight) {
                    yPosition = value < 0 ? yZeroLine + textHeight / 2 : yZeroLine - textHeight / 2;
                }
                else {
                    yPosition = value < 0 ? point.y - point.height / 2 : point.y + point.height / 2;
                }
            }
            yPosition = Math.min(yPosition, yMax);
            yPosition = Math.max(yPosition, yMin);
            // Avoid overlapping texts with same X
            if (!textsPositions[xPosition]) {
                textsPositions[xPosition] = [];
            }
            for (const otherPosition of textsPositions[xPosition] || []) {
                if (Math.abs(otherPosition - yPosition) < 13) {
                    yPosition = value < 0 ? otherPosition + 13 : otherPosition - 13;
                }
            }
            textsPositions[xPosition].push(yPosition);
            ctx.fillStyle = point.options.backgroundColor;
            ctx.strokeStyle = options.background || "#ffffff";
            const valueToDisplay = options.callback(Number(value), dataset, i);
            drawTextWithBackground(valueToDisplay, xPosition, yPosition, ctx);
        }
    }
}
function drawHorizontalBarChartValues(chart, options, ctx) {
    const xMax = chart.chartArea.right;
    const xMin = chart.chartArea.left;
    const textsPositions = {};
    for (const dataset of chart._metasets) {
        if (isTrendLineAxis(dataset.xAxisID)) {
            return; // ignore trend lines
        }
        const xAxisScale = chart.scales[dataset.xAxisID];
        const xZeroLine = xAxisScale.getPixelForValue(0);
        for (let i = 0; i < dataset._parsed.length; i++) {
            const value = Number(dataset._parsed[i].x);
            if (isNaN(value)) {
                continue;
            }
            const displayValue = options.callback(value, dataset, i);
            const point = dataset.data[i];
            const yPosition = point.y;
            const textWidth = computeTextWidth(ctx, displayValue, { fontSize: 12 }, "px");
            const distanceFromAxisOrigin = Math.abs(point.x - xZeroLine);
            const PADDING = 3;
            let xPosition;
            if (distanceFromAxisOrigin < textWidth) {
                xPosition =
                    value < 0 ? xZeroLine - textWidth / 2 - PADDING : xZeroLine + textWidth / 2 + PADDING;
            }
            else {
                xPosition = value < 0 ? point.x + point.width / 2 : point.x - point.width / 2;
                xPosition = Math.min(xPosition, xMax);
                xPosition = Math.max(xPosition, xMin);
            }
            // Avoid overlapping texts with same Y
            if (!textsPositions[yPosition]) {
                textsPositions[yPosition] = [];
            }
            for (const otherPosition of textsPositions[yPosition]) {
                if (Math.abs(otherPosition - xPosition) < textWidth) {
                    xPosition =
                        value < 0 ? otherPosition - textWidth - PADDING : otherPosition + textWidth + PADDING;
                }
            }
            textsPositions[yPosition].push(xPosition);
            ctx.fillStyle = point.options.backgroundColor;
            ctx.strokeStyle = options.background || "#ffffff";
            drawTextWithBackground(displayValue, xPosition, yPosition, ctx);
        }
    }
}
function drawPieChartValues(chart, options, ctx) {
    for (const dataset of chart._metasets) {
        for (let i = 0; i < dataset._parsed.length; i++) {
            const value = Number(dataset._parsed[i]);
            if (isNaN(value) || value === 0) {
                continue;
            }
            const bar = dataset.data[i];
            const { startAngle, endAngle, innerRadius, outerRadius } = bar;
            const midAngle = (startAngle + endAngle) / 2;
            const midRadius = (innerRadius + outerRadius) / 2;
            const x = bar.x + midRadius * Math.cos(midAngle);
            const y = bar.y + midRadius * Math.sin(midAngle);
            const displayValue = options.callback(value, dataset, i);
            const textHeight = 12; // ChartJS default
            const textWidth = computeTextWidth(ctx, displayValue, { fontSize: textHeight }, "px");
            const radius = outerRadius - innerRadius;
            // Check if the text fits in the slice. Not perfect, but good enough heuristic.
            if (textWidth >= radius || radius < textHeight) {
                continue;
            }
            const sliceAngle = endAngle - startAngle;
            const midWidth = 2 * midRadius * Math.tan(sliceAngle / 2);
            if (sliceAngle < Math.PI / 2 && (textWidth >= midWidth || midWidth < textHeight)) {
                continue;
            }
            ctx.fillStyle = chartFontColor(options.background);
            ctx.strokeStyle = options.background || "#ffffff";
            drawTextWithBackground(displayValue, x, y, ctx);
        }
    }
}

const TreeMapChartDefaults = {
    showHeaders: true,
    headerDesign: {
        align: "center",
        fillColor: "#808080",
        bold: true,
        fontSize: 15,
    },
    showValues: true,
    showLabels: true,
    valuesDesign: {
        align: "left",
        verticalAlign: "bottom",
        fontSize: 12,
    },
    coloringOptions: {
        type: "categoryColor",
        colors: [],
        useValueBasedGradient: true,
    },
};

const GHOST_SUNBURST_VALUE = "nullValue";
function getBarChartDatasets(definition, args) {
    const { dataSetsValues } = args;
    const dataSets = [];
    const colors = getChartColorsGenerator(definition, dataSetsValues.length);
    const trendDatasets = [];
    for (const index in dataSetsValues) {
        let { label, data, hidden } = dataSetsValues[index];
        label = definition.dataSets?.[index].label || label;
        const backgroundColor = colors.next();
        const dataset = {
            label,
            data,
            hidden,
            borderColor: definition.background || BACKGROUND_CHART_COLOR,
            borderWidth: definition.stacked ? 1 : 0,
            backgroundColor,
            yAxisID: definition.horizontal ? "y" : definition.dataSets?.[index].yAxisId || "y",
            xAxisID: "x",
        };
        dataSets.push(dataset);
        const trendConfig = definition.dataSets?.[index].trend;
        const trendData = args.trendDataSetsValues?.[index];
        if (!trendConfig?.display || definition.horizontal || !trendData) {
            continue;
        }
        trendDatasets.push(getTrendingLineDataSet(dataset, trendConfig, trendData));
    }
    dataSets.push(...trendDatasets);
    return dataSets;
}
function getWaterfallDatasetAndLabels(definition, args) {
    const { dataSetsValues, labels } = args;
    const negativeColor = definition.negativeValuesColor || CHART_WATERFALL_NEGATIVE_COLOR;
    const positiveColor = definition.positiveValuesColor || CHART_WATERFALL_POSITIVE_COLOR;
    const subTotalColor = definition.subTotalValuesColor || CHART_WATERFALL_SUBTOTAL_COLOR;
    const backgroundColor = [];
    const datasetValues = [];
    const dataset = {
        label: "",
        data: datasetValues,
        backgroundColor,
    };
    const labelsWithSubTotals = [];
    let lastValue = 0;
    for (const dataSetsValue of dataSetsValues) {
        if (dataSetsValue.hidden) {
            continue;
        }
        for (let i = 0; i < dataSetsValue.data.length; i++) {
            const data = dataSetsValue.data[i];
            labelsWithSubTotals.push(labels[i]);
            if (isNaN(Number(data))) {
                datasetValues.push([lastValue, lastValue]);
                backgroundColor.push("");
                continue;
            }
            datasetValues.push([lastValue, data + lastValue]);
            let color = data >= 0 ? positiveColor : negativeColor;
            if (i === 0 && dataSetsValue === dataSetsValues[0] && definition.firstValueAsSubtotal) {
                color = subTotalColor;
            }
            backgroundColor.push(color);
            lastValue += data;
        }
        if (definition.showSubTotals) {
            labelsWithSubTotals.push(_t("Subtotal"));
            datasetValues.push([0, lastValue]);
            backgroundColor.push(subTotalColor);
        }
    }
    return {
        datasets: [dataset],
        labels: labelsWithSubTotals,
    };
}
function getLineChartDatasets(definition, args) {
    const { dataSetsValues, axisType, labels } = args;
    const dataSets = [];
    const areaChart = !!definition.fillArea;
    const stackedChart = !!definition.stacked;
    const trendDatasets = [];
    const colors = getChartColorsGenerator(definition, dataSetsValues.length);
    for (let index = 0; index < dataSetsValues.length; index++) {
        let { label, data, hidden } = dataSetsValues[index];
        label = definition.dataSets?.[index].label || label;
        const color = colors.next();
        if (axisType && ["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 dataset = {
            label,
            data,
            hidden,
            tension: 0, // 0 -> render straight lines, which is much faster
            borderColor: color,
            backgroundColor: areaChart ? setColorAlpha(color, LINE_FILL_TRANSPARENCY) : color,
            pointBackgroundColor: color,
            fill: areaChart ? getFillingMode(index, stackedChart) : false,
            pointRadius: definition.hideDataMarkers ? 0 : LINE_DATA_POINT_RADIUS,
            yAxisID: definition.dataSets?.[index].yAxisId || "y",
        };
        dataSets.push(dataset);
        const trendConfig = definition.dataSets?.[index].trend;
        const trendData = args.trendDataSetsValues?.[index];
        if (!trendConfig?.display || !trendData) {
            continue;
        }
        trendDatasets.push(getTrendingLineDataSet(dataset, trendConfig, trendData));
    }
    dataSets.push(...trendDatasets);
    return dataSets;
}
function getScatterChartDatasets(definition, args) {
    const dataSets = getLineChartDatasets(definition, args);
    for (const dataSet of dataSets) {
        if (!isTrendLineAxis(dataSet.xAxisID)) {
            dataSet.showLine = false;
        }
    }
    return dataSets;
}
function getPieChartDatasets(definition, args) {
    const { dataSetsValues } = args;
    const dataSets = [];
    const dataSetsLength = Math.max(0, ...dataSetsValues.map((ds) => ds?.data?.length ?? 0));
    const backgroundColor = getPieColors(new ColorGenerator(dataSetsLength), dataSetsValues);
    for (const { label, data, hidden } of dataSetsValues) {
        if (hidden)
            continue;
        const dataset = {
            label,
            data,
            borderColor: definition.background || "#FFFFFF",
            backgroundColor,
            hoverOffset: 10,
        };
        dataSets.push(dataset);
    }
    return dataSets;
}
function getComboChartDatasets(definition, args) {
    const { dataSetsValues } = args;
    const dataSets = [];
    const colors = getChartColorsGenerator(definition, dataSetsValues.length);
    const trendDatasets = [];
    for (let index = 0; index < dataSetsValues.length; index++) {
        let { label, data, hidden } = dataSetsValues[index];
        label = definition.dataSets?.[index].label || label;
        const design = definition.dataSets?.[index];
        const color = colors.next();
        const type = design?.type ?? "line";
        const dataset = {
            label: label,
            data,
            hidden,
            borderColor: color,
            backgroundColor: color,
            yAxisID: definition.dataSets?.[index].yAxisId || "y",
            xAxisID: "x",
            type,
            order: type === "bar" ? dataSetsValues.length + index : index,
            pointRadius: definition.hideDataMarkers ? 0 : LINE_DATA_POINT_RADIUS,
        };
        dataSets.push(dataset);
        const trendConfig = definition.dataSets?.[index].trend;
        const trendData = args.trendDataSetsValues?.[index];
        if (!trendConfig?.display || !trendData) {
            continue;
        }
        trendDatasets.push(getTrendingLineDataSet(dataset, trendConfig, trendData));
    }
    dataSets.push(...trendDatasets);
    return dataSets;
}
function getRadarChartDatasets(definition, args) {
    const { dataSetsValues } = args;
    const datasets = [];
    const fill = definition.fillArea ?? false;
    const colors = getChartColorsGenerator(definition, dataSetsValues.length);
    for (let i = 0; i < dataSetsValues.length; i++) {
        let { label, data, hidden } = dataSetsValues[i];
        if (definition.dataSets?.[i]?.label) {
            label = definition.dataSets[i].label;
        }
        const borderColor = colors.next();
        const dataset = {
            label,
            data,
            hidden,
            borderColor,
            backgroundColor: borderColor,
            pointRadius: definition.hideDataMarkers ? 0 : LINE_DATA_POINT_RADIUS,
        };
        if (fill) {
            dataset.backgroundColor = setColorAlpha(borderColor, LINE_FILL_TRANSPARENCY);
            dataset.fill = "start"; // fills from the start of the axes (default is to start at 0)
        }
        datasets.push(dataset);
    }
    return datasets;
}
function getGeoChartDatasets(definition, args) {
    const { availableRegions, dataSetsValues, labels } = args;
    const regionName = definition.region || availableRegions[0]?.id;
    const features = regionName ? args.getGeoJsonFeatures(regionName) : undefined;
    const dataset = {
        outline: features,
        showOutline: !!features,
        data: [],
    };
    if (features && regionName) {
        const labelsAndValues = {};
        if (dataSetsValues[0]) {
            for (let i = 0; i < dataSetsValues[0].data.length; i++) {
                if (!labels[i] || dataSetsValues[0].data[i] === undefined) {
                    continue;
                }
                const featureId = args.geoFeatureNameToId(regionName, labels[i]);
                if (featureId) {
                    labelsAndValues[featureId] = { value: dataSetsValues[0].data[i], label: labels[i] };
                }
            }
        }
        for (const feature of features) {
            if (!feature.id) {
                continue;
            }
            dataset.data.push({
                feature: {
                    ...feature,
                    properties: { name: labelsAndValues[feature.id]?.label },
                },
                value: labelsAndValues[feature.id]?.value,
            });
        }
    }
    return [dataset];
}
function getFunnelChartDatasets(definition, args) {
    const dataSetsValues = args.dataSetsValues[0];
    const labels = args.labels;
    if (!dataSetsValues) {
        return [];
    }
    let { label: datasetLabel, data } = dataSetsValues;
    datasetLabel = definition.dataSets?.[0].label || datasetLabel;
    const dataset = {
        label: datasetLabel,
        data: data.map((value) => (value <= 0 ? [0, 0] : [-value, value])),
        backgroundColor: getFunnelLabelColors(labels, definition.funnelColors),
        yAxisID: "y",
        xAxisID: "x",
        barPercentage: 1,
        categoryPercentage: 1,
        borderColor: definition.background || BACKGROUND_CHART_COLOR,
        borderWidth: 3,
    };
    return [dataset];
}
function getFunnelLabelColors(labels, colors) {
    const colorGenerator = new ColorGenerator(labels.length, colors);
    return labels.map(() => colorGenerator.next());
}
function getSunburstChartDatasets(definition, args) {
    const { dataSetsValues, labels } = args;
    const tree = getSunburstTree(dataSetsValues, labels);
    const data = pyramidizeTree(tree);
    const rootData = data[0] || [];
    const colorGenerator = new ColorGenerator(rootData.length, definition.groupColors || []);
    const groupColors = rootData.map((rawValue) => ({
        label: rawValue.label,
        color: colorGenerator.next(),
    }));
    const dataSets = [];
    for (let i = data.length - 1; i >= 0; i--) {
        const dataset = {
            groupColors,
            parsing: { key: "value" },
            data: data[i],
            borderColor: (ctx) => {
                const data = ctx.type === "data" ? ctx.raw : undefined;
                if (!data || data.label === GHOST_SUNBURST_VALUE) {
                    return COLOR_TRANSPARENT;
                }
                return definition.background || BACKGROUND_CHART_COLOR;
            },
            backgroundColor: (ctx) => {
                const data = ctx.type === "data" ? ctx.raw : undefined;
                if (!data || data.label === GHOST_SUNBURST_VALUE) {
                    return COLOR_TRANSPARENT;
                }
                const rootGroup = data.groups[0];
                return groupColors.find((groupColor) => groupColor.label === rootGroup)?.color;
            },
            hoverOffset: 10,
        };
        dataSets.push(dataset);
    }
    return dataSets;
}
function getDataEntriesFromDatasets(hierarchicalDatasetValues, values) {
    const entries = [];
    const maxDatasetLength = Math.max(...hierarchicalDatasetValues.map((ds) => ds.data.length));
    for (let i = 0; i < maxDatasetLength; i++) {
        entries[i] = {};
        for (let j = 0; j < hierarchicalDatasetValues.length; j++) {
            const groupBy = hierarchicalDatasetValues[j].data[i] === null
                ? GHOST_SUNBURST_VALUE
                : String(hierarchicalDatasetValues[j].data[i]);
            entries[i][j] = groupBy;
        }
        entries[i].value = Number(values[i]);
    }
    return entries;
}
function getSunburstTree(hierarchicalDatasetValues, values) {
    const entries = getDataEntriesFromDatasets(hierarchicalDatasetValues, values);
    return sunburstGroupBy(entries, 0, hierarchicalDatasetValues.length, []);
}
function sunburstGroupBy(entries, index, maxDepth, parentGroups) {
    if (index >= maxDepth) {
        return [];
    }
    const groups = Object.groupBy(entries, (item) => item[index]);
    return Object.keys(groups)
        .map((key) => {
        const total = groups[key]?.reduce((acc, item) => acc + Number(item.value), 0) || 0;
        const itemGroups = [...parentGroups, key];
        const tree = sunburstGroupBy(groups[key] || [], index + 1, maxDepth, [...parentGroups, key]);
        return {
            label: key,
            value: total,
            children: tree,
            groups: itemGroups,
            depth: index,
        };
    })
        .sort((a, b) => b.value - a.value);
}
/**
 * Transform a tree into a "pyramid" array, ie. an array in which each level is an array of nodes at the same depth.
 *
 * Example:
 * ```
 *       A                  [
 *      / \                    [A],
 *     B   C       ===>        [B, C],
 *    / \   \                  [D, E, F],
 *   D   E   F              ]
 *  ```
 */
function pyramidizeTree(tree) {
    const flattened = [];
    const queue = [...tree];
    while (queue.length > 0) {
        const node = queue.shift();
        if (!node) {
            continue;
        }
        if (!flattened[node.depth]) {
            flattened[node.depth] = [];
        }
        flattened[node.depth].push(node);
        if (node.children) {
            queue.push(...node.children);
        }
    }
    return flattened;
}
function getTreeMapChartDatasets(definition, args) {
    const { dataSetsValues, labels, locale, axisFormats } = args;
    const localeFormat = { locale, format: axisFormats?.y };
    if (dataSetsValues.length === 0) {
        return [];
    }
    const tree = getSunburstTree(dataSetsValues, labels).sort((a, b) => b.value - a.value);
    const groupColors = getTreeMapGroupColors(definition, tree);
    const datasetEntries = [];
    const maxDatasetLength = Math.max(...dataSetsValues.map((ds) => ds.data.length));
    for (let i = 0; i < maxDatasetLength; i++) {
        datasetEntries[i] = {};
        for (let j = 0; j < dataSetsValues.length; j++) {
            datasetEntries[i][j] = dataSetsValues[j].data[i]
                ? String(dataSetsValues[j].data[i])
                : undefined;
        }
        datasetEntries[i].value = Number(labels[i]);
    }
    const showLabels = definition.showLabels ?? TreeMapChartDefaults.showLabels;
    const showValues = definition.showValues ?? TreeMapChartDefaults.showValues;
    const coloringOption = definition.coloringOptions || TreeMapChartDefaults.coloringOptions;
    let colorScale;
    if (coloringOption?.type === "colorScale") {
        colorScale = getTreeMapColorScale(tree, coloringOption);
    }
    const dataSets = [
        {
            data: [],
            tree: datasetEntries,
            labels: {
                display: showLabels || showValues,
                overflow: "hidden",
                ...getTextStyle(definition.valuesDesign, TreeMapChartDefaults.valuesDesign),
                formatter: (ctx) => {
                    return [
                        showLabels ? ctx.raw.g : undefined, // group name
                        showValues ? formatValue(ctx.raw.v, localeFormat) : undefined, // formatted value
                    ].filter(isDefined);
                },
            },
            captions: {
                display: definition.showHeaders ?? TreeMapChartDefaults.showHeaders,
                padding: 6,
                ...getTextStyle(definition.headerDesign, TreeMapChartDefaults.headerDesign),
            },
            key: "value",
            groups: range(0, dataSetsValues.length).map((i) => String(i)),
            borderWidth: 0,
            spacing: 1,
            displayMode: "headerBoxes",
            groupColors,
            backgroundColor: (ctx) => {
                if (ctx.type !== "data") {
                    return "transparent";
                }
                if (!ctx.raw.isLeaf) {
                    return definition.headerDesign?.fillColor || TreeMapChartDefaults.headerDesign?.fillColor;
                }
                if (coloringOption.type === "colorScale") {
                    return colorScale?.(ctx.raw.v) || "#FF0000";
                }
                else if (coloringOption.type === "categoryColor") {
                    return getTreeMapElementColor(ctx, tree, coloringOption, groupColors);
                }
                throw new Error(`Unsupported coloring option type}`);
            },
        },
    ];
    return dataSets;
}
function getTextStyle(design, defaultDesign) {
    const dynamicColor = (ctx) => {
        const backgroundColor = ctx.element.options.backgroundColor;
        return relativeLuminance(backgroundColor) > 0.7 ? "#666666" : "#FFFFFF";
    };
    return {
        align: design?.align ?? defaultDesign?.align,
        position: design?.verticalAlign ?? defaultDesign?.verticalAlign,
        color: design?.color || dynamicColor,
        hoverColor: design?.color || dynamicColor,
        font: {
            weight: design?.bold ?? defaultDesign?.bold ? "bold" : "normal",
            style: design?.italic ?? defaultDesign?.italic ? "italic" : "normal",
            size: design?.fontSize ?? defaultDesign?.fontSize,
        },
    };
}
function getTrendingLineDataSet(dataset, config, data) {
    const defaultBorderColor = colorToRGBA(dataset.backgroundColor);
    defaultBorderColor.a = 1;
    const borderColor = config.color || lightenColor(rgbaToHex(defaultBorderColor), 0.5);
    return {
        type: "line",
        xAxisID: config.type === "trailingMovingAverage"
            ? MOVING_AVERAGE_TREND_LINE_XAXIS_ID
            : TREND_LINE_XAXIS_ID,
        yAxisID: dataset.yAxisID,
        label: dataset.label ? _t("Trend line for %s", dataset.label) : "",
        data,
        order: -1,
        showLine: true,
        pointRadius: 0,
        backgroundColor: borderColor,
        borderColor,
        borderDash: [5, 5],
        borderWidth: undefined,
        fill: false,
        pointBackgroundColor: borderColor,
    };
}
/**
 * If the chart is a stacked area chart, we want to fill until the next dataset.
 * If the chart is a simple area chart, we want to fill until the origin (bottom axis).
 *
 * See https://www.chartjs.org/docs/latest/charts/area.html#filling-modes
 */
function getFillingMode(index, stackedChart) {
    if (!stackedChart) {
        return "origin";
    }
    return index === 0 ? "origin" : "-1";
}
function getChartColorsGenerator(definition, dataSetsSize) {
    return new ColorGenerator(dataSetsSize, definition.dataSets?.map((ds) => ds.backgroundColor) || []);
}
function getTreeMapGroupColors(definition, tree) {
    const colors = definition.coloringOptions?.type === "categoryColor" ? definition.coloringOptions.colors : [];
    const colorGenerator = new ColorGenerator(tree.length, colors);
    return tree.map((node) => ({
        label: node.label,
        color: colorGenerator.next(),
    }));
}
function getTreeMapColorScale(tree, coloringOption) {
    if (tree.length === 0) {
        return undefined;
    }
    const treeNodesByLevel = pyramidizeTree(tree);
    const nodes = treeNodesByLevel[treeNodesByLevel.length - 1];
    const minValue = Math.min(...nodes.map((node) => node.value));
    const maxValue = Math.max(...nodes.map((node) => node.value));
    if (Number.isFinite(minValue) && Number.isFinite(maxValue)) {
        const colorThresholds = [{ value: minValue, color: coloringOption.minColor }];
        if (coloringOption.midColor) {
            const midValue = (minValue + maxValue) / 2;
            colorThresholds.push({ value: midValue, color: coloringOption.midColor });
        }
        colorThresholds.push({ value: maxValue, color: coloringOption.maxColor });
        return getColorScale(colorThresholds);
    }
    return undefined;
}
function getTreeMapElementColor(ctx, tree, coloringOption, categoryColors) {
    const rootCategory = ctx.raw._data.children[0][0];
    const baseColor = categoryColors.find((color) => color.label === rootCategory)?.color;
    if (!baseColor || !coloringOption.useValueBasedGradient) {
        return baseColor || "#FF0000";
    }
    const rootNode = tree.find((node) => node.label === rootCategory);
    if (!rootNode || !rootNode.children.length) {
        return baseColor;
    }
    const treeNodesByLevel = pyramidizeTree(rootNode.children);
    const leafValues = treeNodesByLevel[treeNodesByLevel.length - 1];
    const max = Math.max(...leafValues.map((node) => node.value));
    const min = Math.min(...leafValues.map((node) => node.value));
    if (min === max || !isFinite(min) || !isFinite(max)) {
        return baseColor;
    }
    const value = Number(ctx.raw.v) || 0;
    const factor = ((value - max) / (min - max)) * 0.5;
    return lightenColor(baseColor, factor);
}

/**
 * When a chart element is hovered (active), this plugin also activates all of its child elements and
 * lightens the color of the other elements.
 */
const sunburstHoverPlugin = {
    id: "sunburstHoverPlugin",
    afterEvent(chart, args, options) {
        if (!options.enabled) {
            return;
        }
        const chartActiveElements = chart.getActiveElements();
        let activeDataPoints = chartActiveElements.map((el) => ({
            datasetIndex: el.datasetIndex,
            index: el.index,
        }));
        for (const activeEl of chartActiveElements) {
            const activeDataset = chart.data.datasets[activeEl.datasetIndex];
            const activeData = activeDataset.data[activeEl.index];
            for (let datasetIndex = 0; datasetIndex < chart.data.datasets.length; datasetIndex++) {
                const dataset = chart.data.datasets[datasetIndex];
                for (let index = 0; index < dataset.data.length; index++) {
                    const data = dataset.data[index];
                    if (isChildGroup(activeData.groups, data.groups)) {
                        activeDataPoints.push({ datasetIndex, index });
                    }
                }
            }
        }
        activeDataPoints = activeDataPoints.filter((point) => {
            const { datasetIndex, index } = point;
            const data = chart.data.datasets[datasetIndex].data[index];
            return data.label !== GHOST_SUNBURST_VALUE;
        });
        chart.setActiveElements(activeDataPoints);
        for (const metaSet of chart.getSortedVisibleDatasetMetas()) {
            for (const arcElement of metaSet.data) {
                const context = arcElement["$context"];
                const { datasetIndex, index, dataset, raw } = context;
                if (raw.label === GHOST_SUNBURST_VALUE) {
                    continue;
                }
                const originalBackgroundColor = typeof dataset.backgroundColor === "function"
                    ? dataset.backgroundColor(context)
                    : dataset.backgroundColor;
                if (activeDataPoints.length &&
                    !activeDataPoints.some((el) => el.datasetIndex === datasetIndex && el.index === index)) {
                    arcElement.options.backgroundColor = lightenColor(originalBackgroundColor, 0.5);
                }
                else {
                    arcElement.options.backgroundColor = originalBackgroundColor;
                }
            }
        }
    },
};
function isChildGroup(parentGroup, childGroup) {
    return (childGroup.length > parentGroup.length &&
        parentGroup.every((group, i) => group === childGroup[i]));
}

const Y_PADDING = 3;
const sunburstLabelsPlugin = {
    id: "sunburstLabelsPlugin",
    afterDatasetsDraw(chart, args, options) {
        if ((!options.showValues && !options.showLabels) || chart.config.type !== "doughnut") {
            return;
        }
        const ctx = chart.ctx;
        drawSunburstChartValues(chart, options, ctx);
    },
};
function drawSunburstChartValues(chart, options, ctx) {
    const style = options.style;
    const fontSize = style.fontSize || 13;
    const lineHeight = fontSize + Y_PADDING;
    for (const dataset of chart._metasets) {
        for (let i = 0; i < dataset._dataset.data.length; i++) {
            const rawData = dataset._dataset.data[i];
            if (rawData.label === GHOST_SUNBURST_VALUE) {
                continue;
            }
            const valuesToDisplay = [
                options.showLabels ? rawData.label : undefined,
                options.showValues ? options.callback(rawData.value, "y") : undefined,
            ].filter(isDefined);
            const arc = dataset.data[i];
            let { startAngle, endAngle, innerRadius, outerRadius, circumference } = arc;
            // Same computations as in ChartJs ArcElement's draw method. Don't ask me why they divide by 4.
            const offset = arc.options.offset / 4;
            const fix = 1 - Math.sin(Math.min(Math.PI, circumference || 0));
            const radiusOffset = offset * fix;
            innerRadius += radiusOffset;
            outerRadius += radiusOffset;
            const midAngle = (startAngle + endAngle) / 2;
            const midRadius = (innerRadius + outerRadius) / 2;
            const availableWidth = (outerRadius - innerRadius) * 0.9;
            const angle = endAngle - startAngle;
            const availableHeight = angle >= Math.PI ? outerRadius : Math.sin(angle / 2) * innerRadius * 2;
            if (availableHeight < valuesToDisplay.length * lineHeight) {
                continue;
            }
            ctx.save();
            const centerOffset = { x: Math.cos(midAngle) * offset, y: Math.sin(midAngle) * offset };
            const centerX = chart.chartArea.left + chart.chartArea.width / 2 + centerOffset.x;
            const centerY = chart.chartArea.top + chart.chartArea.height / 2 + centerOffset.y;
            ctx.translate(centerX, centerY);
            let x;
            if (midAngle > Math.PI / 2) {
                ctx.rotate(midAngle - Math.PI);
                x = -midRadius;
            }
            else {
                x = midRadius;
                ctx.rotate(midAngle);
            }
            const backgroundColor = arc.options.backgroundColor;
            const defaultColor = relativeLuminance(backgroundColor) > 0.7 ? "#666666" : "#FFFFFF";
            ctx.fillStyle = style.textColor || defaultColor;
            ctx.textAlign = "center";
            ctx.textBaseline = "middle";
            ctx.font = getDefaultContextFont(fontSize, style.bold, style.italic);
            const y = -((valuesToDisplay.length - 1) * lineHeight) / 2;
            for (let j = 0; j < valuesToDisplay.length; j++) {
                const fittedText = sliceTextToFitWidth(ctx, availableWidth, valuesToDisplay[j], style, "px");
                ctx.fillText(fittedText, x, y + j * lineHeight);
            }
            ctx.restore();
        }
    }
}

/** This is a chartJS plugin that will draw connector lines between the bars of a Waterfall chart */
const waterfallLinesPlugin = {
    id: "waterfallLinesPlugin",
    beforeDraw(chart, args, options) {
        if (!options.showConnectorLines) {
            return;
        }
        // Note: private properties are not in the typing of chartJS (and some of the existing types are missing properties)
        // so we don't type anything in this file
        const drawData = chart._metasets?.[0]?.data;
        if (!drawData) {
            return;
        }
        const ctx = chart.ctx;
        ctx.save();
        ctx.setLineDash([3, 2]);
        for (let i = 0; i < drawData.length; i++) {
            const bar = drawData[i];
            if (bar.height === 0) {
                continue;
            }
            const nextBar = getNextNonEmptyBar(drawData, i);
            if (!nextBar) {
                break;
            }
            const rect = getBarElementRect(bar);
            const nextBarRect = getBarElementRect(nextBar);
            const rawBarValues = bar.$context.raw;
            const value = rawBarValues[1] - rawBarValues[0];
            const lineY = Math.round(value < 0 ? rect.bottom - 1 : rect.top);
            const lineStart = Math.round(rect.right);
            const lineEnd = Math.round(nextBarRect.left);
            ctx.strokeStyle = "#999";
            ctx.beginPath();
            ctx.moveTo(lineStart + 1, lineY + 0.5);
            ctx.lineTo(lineEnd, lineY + 0.5);
            ctx.stroke();
        }
        ctx.restore();
    },
};
function getBarElementRect(bar) {
    const flipped = bar.base < bar.y; // Bar are flipped for negative values in the dataset
    return {
        left: bar.x - bar.width / 2,
        right: bar.x + bar.width / 2,
        bottom: flipped ? bar.base + bar.height : bar.y + bar.height,
        top: flipped ? bar.base : bar.y,
    };
}
function getNextNonEmptyBar(bars, startIndex) {
    return bars.find((bar, i) => i > startIndex && bar.height !== 0);
}

css /* scss */ `
  .o-spreadsheet {
    .o-chart-custom-tooltip {
      font-size: 12px;
      background-color: #fff;
      z-index: ${ComponentsImportance.FigureTooltip};
    }
  }
`;
chartJsExtensionRegistry.add("chartShowValuesPlugin", {
    register: (Chart) => Chart.register(chartShowValuesPlugin),
    unregister: (Chart) => Chart.unregister(chartShowValuesPlugin),
});
chartJsExtensionRegistry.add("waterfallLinesPlugin", {
    register: (Chart) => Chart.register(waterfallLinesPlugin),
    unregister: (Chart) => Chart.unregister(waterfallLinesPlugin),
});
chartJsExtensionRegistry.add("funnelController", {
    register: (Chart) => Chart.register(getFunnelChartController()),
    unregister: (Chart) => Chart.unregister(getFunnelChartController()),
});
chartJsExtensionRegistry.add("funnelElement", {
    register: (Chart) => Chart.register(getFunnelChartElement()),
    unregister: (Chart) => Chart.unregister(getFunnelChartElement()),
});
chartJsExtensionRegistry.add("funnelTooltipPositioner", {
    register: (Chart) => (Chart.Tooltip.positioners.funnelTooltipPositioner = funnelTooltipPositioner),
    // @ts-expect-error
    unregister: (Chart) => (Chart.Tooltip.positioners.funnelTooltipPositioner = undefined),
});
chartJsExtensionRegistry.add("sunburstLabelsPlugin", {
    register: (Chart) => Chart.register(sunburstLabelsPlugin),
    unregister: (Chart) => Chart.unregister(sunburstLabelsPlugin),
});
chartJsExtensionRegistry.add("sunburstHoverPlugin", {
    register: (Chart) => Chart.register(sunburstHoverPlugin),
    unregister: (Chart) => Chart.unregister(sunburstHoverPlugin),
});
class ChartJsComponent extends Component {
    static template = "o-spreadsheet-ChartJsComponent";
    static props = {
        chartId: String,
        isFullScreen: { type: Boolean, optional: true },
    };
    canvas = useRef("graphContainer");
    chart;
    currentRuntime;
    animationStore;
    currentDevicePixelRatio = window.devicePixelRatio;
    get background() {
        return this.chartRuntime.background;
    }
    get canvasStyle() {
        return `background-color: ${this.background}`;
    }
    get chartRuntime() {
        const runtime = this.env.model.getters.getChartRuntime(this.props.chartId);
        if (!("chartJsConfig" in runtime)) {
            throw new Error("Unsupported chart runtime");
        }
        return runtime;
    }
    setup() {
        if (this.shouldAnimate) {
            this.animationStore = useStore(ChartAnimationStore);
        }
        onMounted(() => {
            registerChartJSExtensions();
            const runtime = this.chartRuntime;
            this.currentRuntime = runtime;
            // Note: chartJS modify the runtime in place, so it's important to give it a copy
            this.createChart(deepCopy(runtime));
        });
        onWillUnmount(this.unmount.bind(this));
        useEffect(() => {
            const runtime = this.chartRuntime;
            if (runtime !== this.currentRuntime) {
                if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
                    this.chart?.destroy();
                    this.createChart(deepCopy(runtime));
                }
                else {
                    this.updateChartJs(deepCopy(runtime));
                }
                this.currentRuntime = runtime;
            }
            else if (this.currentDevicePixelRatio !== window.devicePixelRatio) {
                this.currentDevicePixelRatio = window.devicePixelRatio;
                this.updateChartJs(deepCopy(this.currentRuntime));
            }
        });
    }
    unmount() {
        this.chart?.destroy();
    }
    get shouldAnimate() {
        return this.env.model.getters.isDashboard();
    }
    createChart(chartRuntime) {
        let chartData = chartRuntime.chartJsConfig;
        if (this.shouldAnimate && this.animationStore) {
            const chartType = this.env.model.getters.getChart(this.props.chartId)?.type;
            if (chartType && this.animationStore.animationPlayed[this.animationChartId] !== chartType) {
                chartData = this.enableAnimationInChartData(chartData);
                this.animationStore.disableAnimationForChart(this.animationChartId, chartType);
            }
        }
        const canvas = this.canvas.el;
        const ctx = canvas.getContext("2d");
        this.chart = new window.Chart(ctx, chartData);
    }
    updateChartJs(chartRuntime) {
        let chartData = chartRuntime.chartJsConfig;
        if (this.shouldAnimate) {
            const chartType = this.env.model.getters.getChart(this.props.chartId)?.type;
            if (chartType && this.hasChartDataChanged() && this.animationStore) {
                chartData = this.enableAnimationInChartData(chartData);
                this.animationStore.disableAnimationForChart(this.animationChartId, chartType);
            }
        }
        if (chartData.data && chartData.data.datasets) {
            this.chart.data = chartData.data;
            if (chartData.options?.plugins?.title) {
                this.chart.config.options.plugins.title = chartData.options.plugins.title;
            }
        }
        else {
            this.chart.data.datasets = [];
        }
        this.chart.config.options = chartData.options;
        this.chart.update();
    }
    hasChartDataChanged() {
        return !deepEquals(this.getChartDataInRuntime(this.currentRuntime), this.getChartDataInRuntime(this.chartRuntime));
    }
    enableAnimationInChartData(chartData) {
        return {
            ...chartData,
            options: { ...chartData.options, animation: { animateRotate: true } },
        };
    }
    getChartDataInRuntime(runtime) {
        const data = runtime.chartJsConfig.data;
        return {
            labels: data.labels,
            dataset: data.datasets.map((dataset) => ({
                data: dataset.data,
                label: dataset.label,
                tree: dataset.tree,
            })),
        };
    }
    get animationChartId() {
        return this.props.isFullScreen ? this.props.chartId + "-fullscreen" : this.props.chartId;
    }
}

/**
 * AbstractChart is the class from which every Chart should inherit.
 * The role of this class is to maintain the state of each chart.
 */
class AbstractChart {
    sheetId;
    title;
    getters;
    humanize;
    constructor(definition, sheetId, getters) {
        this.title = definition.title;
        this.sheetId = sheetId;
        this.getters = getters;
        this.humanize = definition.humanize ?? true;
    }
    /**
     * 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(chartSheetId, definition, applyChange) {
        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");
    }
    getCommonDataSetAttributesForExcel(labelRange, dataSets, shouldRemoveFirstLabel) {
        const excelDataSets = dataSets
            .map((ds) => toExcelDataset(this.getters, ds))
            .filter((ds) => ds.range !== "" && ds.range !== CellErrorType.InvalidReference);
        const excelLabelRange = toExcelLabelRange(this.getters, labelRange, shouldRemoveFirstLabel);
        return {
            dataSets: excelDataSets,
            labelRange: excelLabelRange,
        };
    }
}

function getBaselineText(baseline, keyValue, baselineMode, humanizeNumbers, locale) {
    if (!baseline) {
        return "";
    }
    else if (baselineMode === "text" ||
        keyValue?.type !== CellValueType.number ||
        baseline.type !== CellValueType.number) {
        if (humanizeNumbers) {
            return humanizeNumber(baseline, locale);
        }
        return baseline.formattedValue;
    }
    let { value, format } = baseline;
    if (baselineMode === "progress") {
        value = keyValue.value / value;
        format = "0.0%";
    }
    else {
        value = Math.abs(keyValue.value - value);
        if (baselineMode === "percentage" && value !== 0) {
            value = value / baseline.value;
        }
        if (baselineMode === "percentage") {
            format = "0.0%";
        }
        if (!format) {
            value = Math.round(value * 100) / 100;
        }
    }
    if (humanizeNumbers) {
        return humanizeNumber({ value, format }, locale);
    }
    return formatValue(value, { format, locale });
}
function getKeyValueText(keyValueCell, humanizeNumbers, locale) {
    if (!keyValueCell) {
        return "";
    }
    if (humanizeNumbers) {
        return humanizeNumber(keyValueCell, locale);
    }
    return keyValueCell.formattedValue ?? String(keyValueCell.value ?? "");
}
function getBaselineColor(baseline, baselineMode, keyValue, colorUp, colorDown) {
    if (baselineMode === "text" ||
        baselineMode === "progress" ||
        baseline?.type !== CellValueType.number ||
        keyValue?.type !== CellValueType.number) {
        return undefined;
    }
    const diff = keyValue.value - baseline.value;
    if (diff > 0) {
        return colorUp;
    }
    else if (diff < 0) {
        return colorDown;
    }
    return undefined;
}
function getBaselineArrowDirection(baseline, keyValue, baselineMode) {
    if (baselineMode === "text" ||
        baseline?.type !== CellValueType.number ||
        keyValue?.type !== CellValueType.number) {
        return "neutral";
    }
    const diff = keyValue.value - baseline.value;
    if (diff > 0) {
        return "up";
    }
    else if (diff < 0) {
        return "down";
    }
    return "neutral";
}
function checkKeyValue(definition) {
    return definition.keyValue && !rangeReference.test(definition.keyValue)
        ? "InvalidScorecardKeyValue" /* CommandResult.InvalidScorecardKeyValue */
        : "Success" /* CommandResult.Success */;
}
function checkBaseline(definition) {
    return definition.baseline && !rangeReference.test(definition.baseline)
        ? "InvalidScorecardBaseline" /* CommandResult.InvalidScorecardBaseline */
        : "Success" /* CommandResult.Success */;
}
const arrowDownPath = new window.Path2D("M8.6 4.8a.5.5 0 0 1 0 .75l-3.9 3.9a.5 .5 0 0 1 -.75 0l-3.8 -3.9a.5 .5 0 0 1 0 -.75l.4-.4a.5.5 0 0 1 .75 0l2.3 2.4v-5.7c0-.25.25-.5.5-.5h.6c.25 0 .5.25.5.5v5.8l2.3 -2.4a.5.5 0 0 1 .75 0z");
const arrowUpPath = new window.Path2D("M8.7 5.5a.5.5 0 0 0 0-.75l-3.8-4a.5.5 0 0 0-.75 0l-3.8 4a.5.5 0 0 0 0 .75l.4.4a.5.5 0 0 0 .75 0l2.3-2.4v5.8c0 .25.25.5.5.5h.6c.25 0 .5-.25.5-.5v-5.8l2.2 2.4a.5.5 0 0 0 .75 0z");
let ScorecardChart$1 = class ScorecardChart extends AbstractChart {
    keyValue;
    keyDescr;
    baseline;
    baselineMode;
    baselineDescr;
    progressBar = false;
    background;
    baselineColorUp;
    baselineColorDown;
    fontColor;
    humanize;
    type = "scorecard";
    constructor(definition, sheetId, getters) {
        super(definition, sheetId, getters);
        this.keyValue = createValidRange(getters, sheetId, definition.keyValue);
        this.keyDescr = definition.keyDescr;
        this.baseline = createValidRange(getters, sheetId, definition.baseline);
        this.baselineMode = definition.baselineMode;
        this.baselineDescr = definition.baselineDescr;
        this.background = definition.background;
        this.baselineColorUp = definition.baselineColorUp ?? DEFAULT_SCORECARD_BASELINE_COLOR_UP;
        this.baselineColorDown = definition.baselineColorDown ?? DEFAULT_SCORECARD_BASELINE_COLOR_DOWN;
        this.humanize = definition.humanize ?? true;
    }
    static validateChartDefinition(validator, definition) {
        return validator.checkValidations(definition, checkKeyValue, checkBaseline);
    }
    static getDefinitionFromContextCreation(context) {
        return {
            background: context.background,
            type: "scorecard",
            keyValue: context.range?.[0]?.dataRange,
            title: context.title || { text: "" },
            baselineMode: DEFAULT_SCORECARD_BASELINE_MODE,
            baselineColorUp: DEFAULT_SCORECARD_BASELINE_COLOR_UP,
            baselineColorDown: DEFAULT_SCORECARD_BASELINE_COLOR_DOWN,
            baseline: context.auxiliaryRange || "",
            humanize: context.humanize,
        };
    }
    static transformDefinition(chartSheetId, definition, applyChange) {
        let baseline;
        let keyValue;
        if (definition.baseline) {
            const { changeType, range: adaptedRange } = adaptStringRange(chartSheetId, definition.baseline, applyChange);
            if (changeType !== "REMOVE") {
                baseline = adaptedRange;
            }
        }
        if (definition.keyValue) {
            const { changeType, range: adaptedRange } = adaptStringRange(chartSheetId, definition.keyValue, applyChange);
            if (changeType !== "REMOVE") {
                keyValue = adaptedRange;
            }
        }
        return {
            ...definition,
            baseline,
            keyValue,
        };
    }
    duplicateInDuplicatedSheet(newSheetId) {
        const baseline = duplicateLabelRangeInDuplicatedSheet(this.sheetId, newSheetId, this.baseline);
        const keyValue = duplicateLabelRangeInDuplicatedSheet(this.sheetId, newSheetId, this.keyValue);
        const definition = this.getDefinitionWithSpecificRanges(baseline, keyValue, newSheetId);
        return new ScorecardChart(definition, newSheetId, this.getters);
    }
    copyInSheetId(sheetId) {
        const definition = this.getDefinitionWithSpecificRanges(this.baseline, this.keyValue, sheetId);
        return new ScorecardChart(definition, sheetId, this.getters);
    }
    getDefinition() {
        return this.getDefinitionWithSpecificRanges(this.baseline, this.keyValue);
    }
    getContextCreation() {
        return {
            ...this,
            range: this.keyValue
                ? [{ dataRange: 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,
            keyDescr: this.keyDescr,
            humanize: this.humanize,
        };
    }
    getDefinitionForExcel() {
        // This kind of graph is not exportable in Excel
        return undefined;
    }
    updateRanges({ applyChange }) {
        const baseline = adaptChartRange(this.baseline, applyChange);
        const keyValue = adaptChartRange(this.keyValue, applyChange);
        if (this.baseline === baseline && this.keyValue === keyValue) {
            return this;
        }
        const definition = this.getDefinitionWithSpecificRanges(baseline, keyValue);
        return new ScorecardChart(definition, this.sheetId, this.getters);
    }
};
function drawScoreChart(structure, canvas) {
    const ctx = canvas.getContext("2d");
    const dpr = window.devicePixelRatio || 1;
    canvas.width = dpr * structure.canvas.width;
    canvas.height = dpr * structure.canvas.height;
    ctx.scale(dpr, dpr);
    const availableWidth = structure.canvas.width - CHART_PADDING;
    ctx.fillStyle = structure.canvas.backgroundColor;
    ctx.fillRect(0, 0, structure.canvas.width, structure.canvas.height);
    if (structure.title) {
        ctx.font = structure.title.style.font;
        ctx.fillStyle = structure.title.style.color;
        const baseline = ctx.textBaseline;
        ctx.textBaseline = "middle";
        ctx.fillText(clipTextWithEllipsis(ctx, structure.title.text, availableWidth - structure.title.position.x), structure.title.position.x, structure.title.position.y);
        ctx.textBaseline = baseline;
    }
    if (structure.baseline) {
        ctx.font = structure.baseline.style.font;
        ctx.fillStyle = structure.baseline.style.color;
        drawDecoratedText(ctx, structure.baseline.text, structure.baseline.position, structure.baseline.style.underline, structure.baseline.style.strikethrough);
    }
    if (structure.baselineArrow && structure.baselineArrow.style.size > 0) {
        ctx.save();
        ctx.fillStyle = structure.baselineArrow.style.color;
        ctx.translate(structure.baselineArrow.position.x, structure.baselineArrow.position.y);
        // This ratio is computed according to the original svg size and the final size we want
        const ratio = structure.baselineArrow.style.size / 10;
        ctx.scale(ratio, ratio);
        switch (structure.baselineArrow.direction) {
            case "down": {
                ctx.fill(arrowDownPath);
                break;
            }
            case "up": {
                ctx.fill(arrowUpPath);
                break;
            }
        }
        ctx.restore();
    }
    if (structure.baselineDescr) {
        const descr = structure.baselineDescr;
        ctx.font = descr.style.font;
        ctx.fillStyle = descr.style.color;
        ctx.fillText(clipTextWithEllipsis(ctx, descr.text, availableWidth - descr.position.x), descr.position.x, descr.position.y);
    }
    if (structure.key) {
        ctx.font = structure.key.style.font;
        ctx.fillStyle = structure.key.style.color;
        drawDecoratedText(ctx, clipTextWithEllipsis(ctx, structure.key.text, availableWidth - structure.key.position.x), structure.key.position, structure.key.style.underline, structure.key.style.strikethrough);
    }
    if (structure.keyDescr) {
        const descr = structure.keyDescr;
        ctx.font = structure.keyDescr?.style.font ?? descr.style.font;
        ctx.fillStyle = descr.style.color;
        ctx.fillText(clipTextWithEllipsis(ctx, descr.text, availableWidth - descr.position.x), descr.position.x, descr.position.y);
    }
    if (structure.progressBar) {
        ctx.fillStyle = structure.progressBar.style.backgroundColor;
        ctx.beginPath();
        ctx.roundRect(structure.progressBar.position.x, structure.progressBar.position.y, structure.progressBar.dimension.width, structure.progressBar.dimension.height, structure.progressBar.dimension.height / 2);
        ctx.fill();
        ctx.fillStyle = structure.progressBar.style.color;
        ctx.beginPath();
        if (structure.progressBar.value > 0) {
            ctx.roundRect(structure.progressBar.position.x, structure.progressBar.position.y, structure.progressBar.dimension.width *
                Math.max(0, Math.min(1.0, structure.progressBar.value)), structure.progressBar.dimension.height, structure.progressBar.dimension.height / 2);
        }
        else {
            const width = structure.progressBar.dimension.width *
                Math.max(0, Math.min(1.0, -structure.progressBar.value));
            ctx.roundRect(structure.progressBar.position.x + structure.progressBar.dimension.width - width, structure.progressBar.position.y, width, structure.progressBar.dimension.height, structure.progressBar.dimension.height / 2);
        }
        ctx.fill();
    }
}
function createScorecardChartRuntime(chart, getters) {
    let formattedKeyValue = "";
    let keyValueCell;
    const locale = getters.getLocale();
    if (chart.keyValue) {
        const keyValuePosition = {
            sheetId: chart.keyValue.sheetId,
            col: chart.keyValue.zone.left,
            row: chart.keyValue.zone.top,
        };
        keyValueCell = getters.getEvaluatedCell(keyValuePosition);
        formattedKeyValue = getKeyValueText(keyValueCell, chart.humanize ?? true, locale);
    }
    let baselineCell;
    const baseline = chart.baseline;
    if (baseline) {
        const baselinePosition = {
            sheetId: baseline.sheetId,
            col: baseline.zone.left,
            row: baseline.zone.top,
        };
        baselineCell = getters.getEvaluatedCell(baselinePosition);
    }
    const { background, fontColor } = getters.getStyleOfSingleCellChart(chart.background, chart.keyValue);
    const baselineDisplay = getBaselineText(baselineCell, keyValueCell, chart.baselineMode, chart.humanize ?? true, locale);
    const baselineValue = chart.baselineMode === "progress" && isNumber(baselineDisplay, locale)
        ? toNumber(baselineDisplay, locale)
        : 0;
    return {
        title: {
            ...chart.title,
            text: chart.title.text ? getters.dynamicTranslate(chart.title.text) : "",
        },
        keyValue: formattedKeyValue,
        keyDescr: chart.keyDescr?.text ? getters.dynamicTranslate(chart.keyDescr.text) : "",
        baselineDisplay,
        baselineArrow: getBaselineArrowDirection(baselineCell, keyValueCell, chart.baselineMode),
        baselineColor: getBaselineColor(baselineCell, chart.baselineMode, keyValueCell, chart.baselineColorUp, chart.baselineColorDown),
        baselineDescr: chart.baselineMode !== "progress" && chart.baselineDescr?.text
            ? getters.dynamicTranslate(chart.baselineDescr.text)
            : "",
        fontColor,
        background,
        baselineStyle: {
            ...(chart.baselineMode !== "percentage" && chart.baselineMode !== "progress" && baseline
                ? getters.getCellComputedStyle({
                    sheetId: baseline.sheetId,
                    col: baseline.zone.left,
                    row: baseline.zone.top,
                })
                : undefined),
            fontSize: chart.baselineDescr?.fontSize,
            align: chart.baselineDescr?.align,
        },
        baselineDescrStyle: { textColor: chart.baselineDescr?.color, ...chart.baselineDescr },
        keyValueStyle: {
            ...(chart.keyValue
                ? getters.getCellComputedStyle({
                    sheetId: chart.keyValue.sheetId,
                    col: chart.keyValue.zone.left,
                    row: chart.keyValue.zone.top,
                })
                : undefined),
            fontSize: chart.keyDescr?.fontSize,
            align: chart.keyDescr?.align,
        },
        keyValueDescrStyle: { textColor: chart.keyDescr?.color, ...chart.keyDescr },
        progressBar: chart.baselineMode === "progress"
            ? {
                value: baselineValue,
                color: baselineValue > 0 ? chart.baselineColorUp : chart.baselineColorDown,
            }
            : undefined,
    };
}

/* Padding at the border of the chart */
const BOTTOM_PADDING_RATIO = 0.05;
function formatBaselineDescr(baselineDescr, baseline) {
    const _baselineDescr = baselineDescr || "";
    return baseline && _baselineDescr ? " " + _baselineDescr : _baselineDescr;
}
function getScorecardConfiguration({ width, height }, runtime) {
    const designer = new ScorecardChartConfigBuilder({ width, height }, runtime);
    return designer.computeDesign();
}
class ScorecardChartConfigBuilder {
    runtime;
    context;
    width;
    height;
    constructor({ width, height }, runtime) {
        this.runtime = runtime;
        const canvas = document.createElement("canvas");
        this.width = canvas.width = width;
        this.height = canvas.height = height;
        this.context = canvas.getContext("2d");
    }
    computeDesign() {
        const structure = {
            canvas: {
                width: this.width,
                height: this.height,
                backgroundColor: this.backgroundColor,
            },
        };
        const style = this.getTextStyles();
        let titleHeight = 0;
        if (this.title) {
            let x, titleWidth;
            ({ height: titleHeight, width: titleWidth } = this.getFullTextDimensions(this.title, style.title.font));
            switch (this.runtime.title.align) {
                case "center":
                    x = (this.width - titleWidth) / 2;
                    break;
                case "right":
                    x = this.width - titleWidth - CHART_PADDING;
                    break;
                case "left":
                default:
                    x = CHART_PADDING;
            }
            structure.title = {
                text: this.title,
                style: style.title,
                position: {
                    x,
                    y: CHART_PADDING_BOTTOM + titleHeight / 2,
                },
            };
        }
        const baselineArrowSize = style.baselineArrow?.size ?? 0;
        let { height: baselineHeight, width: baselineWidth } = this.getTextDimensions(this.baseline, style.baselineValue.font);
        if (!this.baseline) {
            baselineHeight = this.getTextDimensions(this.baselineDescr, style.baselineDescr.font).height;
        }
        const baselineDescrWidth = this.getTextDimensions(this.baselineDescr, style.baselineDescr.font).width;
        let baselineX;
        switch (this.runtime.baselineStyle?.align) {
            case "right":
                baselineX = this.width - CHART_PADDING - baselineDescrWidth - baselineWidth;
                break;
            case "left":
                baselineX = CHART_PADDING + baselineArrowSize;
                break;
            default:
                baselineX = (this.width - baselineWidth - baselineDescrWidth + baselineArrowSize) / 2;
        }
        if (this.baseline) {
            structure.baseline = {
                text: this.baseline,
                style: style.baselineValue,
                position: {
                    x: baselineX,
                    y: this.keyValue
                        ? this.height * (1 - BOTTOM_PADDING_RATIO * (this.runtime.progressBar ? 1 : 2))
                        : this.height - (this.height - titleHeight - baselineHeight) / 2 - CHART_PADDING_BOTTOM,
                },
            };
            if (style.baselineArrow && !this.runtime.progressBar) {
                structure.baselineArrow = {
                    direction: this.baselineArrow,
                    style: style.baselineArrow,
                    position: {
                        x: structure.baseline.position.x - baselineArrowSize,
                        y: structure.baseline.position.y - (baselineHeight + baselineArrowSize) / 2,
                    },
                };
            }
        }
        if (structure.baseline && this.baselineDescr) {
            const position = {
                x: structure.baseline.position.x + baselineWidth,
                y: structure.baseline.position.y,
            };
            structure.baselineDescr = {
                text: this.baselineDescr,
                style: style.baselineDescr,
                position,
            };
        }
        let progressBarHeight = 0;
        if (this.runtime.progressBar) {
            progressBarHeight = this.height * 0.05;
            structure.progressBar = {
                position: {
                    x: 2 * CHART_PADDING,
                    y: this.height * (1 - 2 * BOTTOM_PADDING_RATIO) - baselineHeight - progressBarHeight,
                },
                dimension: {
                    height: progressBarHeight,
                    width: this.width - 4 * CHART_PADDING,
                },
                value: this.runtime.progressBar.value,
                style: {
                    color: this.runtime.progressBar.color,
                    backgroundColor: this.secondaryFontColor,
                },
            };
        }
        const { width: keyWidth, height: keyHeight } = this.getFullTextDimensions(this.keyValue, style.keyValue.font);
        const keyDescrWidth = this.getTextDimensions(this.keyDescr, style.keyDescr.font).width;
        let keyX;
        switch (this.runtime.keyValueStyle?.align) {
            case "right":
                keyX = this.width - CHART_PADDING - keyDescrWidth - keyWidth;
                break;
            case "left":
                keyX = CHART_PADDING;
                break;
            default:
                keyX = (this.width - keyWidth - keyDescrWidth) / 2;
        }
        if (this.keyValue) {
            structure.key = {
                text: this.keyValue,
                style: style.keyValue,
                position: {
                    x: Math.max(CHART_PADDING, keyX),
                    y: this.height * (0.5 - BOTTOM_PADDING_RATIO * 2) +
                        CHART_PADDING_BOTTOM / 2 +
                        (titleHeight + keyHeight / 2) / 2,
                },
            };
        }
        if (structure.key && this.keyDescr) {
            const position = {
                x: structure.key.position.x + keyWidth,
                y: structure.key.position.y,
            };
            structure.keyDescr = {
                text: this.keyDescr,
                style: style.keyDescr,
                position,
            };
        }
        return structure;
    }
    get title() {
        return this.runtime.title.text ?? "";
    }
    get keyValue() {
        return this.runtime.keyValue;
    }
    get keyDescr() {
        return formatBaselineDescr(this.runtime.keyDescr, this.keyValue);
    }
    get baseline() {
        return this.runtime.baselineDisplay;
    }
    get baselineDescr() {
        return formatBaselineDescr(this.runtime.baselineDescr, this.baseline);
    }
    get baselineArrow() {
        return this.runtime.baselineArrow;
    }
    get backgroundColor() {
        return this.runtime.background;
    }
    get secondaryFontColor() {
        return chartMutedFontColor(this.backgroundColor);
    }
    getTextDimensions(text, font) {
        this.context.font = font;
        const measure = this.context.measureText(text);
        return {
            width: measure.width,
            height: measure.actualBoundingBoxAscent + measure.actualBoundingBoxDescent,
        };
    }
    getFullTextDimensions(text, font) {
        this.context.font = font;
        const measure = this.context.measureText(text);
        return {
            width: measure.width,
            height: measure.fontBoundingBoxAscent + measure.fontBoundingBoxDescent,
        };
    }
    getTextStyles() {
        const keyValueFontSize = this.runtime.keyValueStyle?.fontSize ?? DEFAULT_SCORECARD_KEY_VALUE_FONT_SIZE;
        const keyValueDescrFontSize = Math.floor(0.9 * keyValueFontSize);
        let baselineValueFontSize = this.runtime.baselineStyle?.fontSize ?? DEFAULT_SCORECARD_BASELINE_FONT_SIZE;
        const baselineDescrFontSize = Math.floor(0.9 * baselineValueFontSize);
        if (this.runtime.progressBar) {
            baselineValueFontSize /= 1.5;
        }
        return {
            title: {
                font: getDefaultContextFont(this.runtime.title.fontSize ?? SCORECARD_CHART_TITLE_FONT_SIZE, this.runtime.title.bold, this.runtime.title.italic),
                color: this.runtime.title.color ?? this.secondaryFontColor,
            },
            keyValue: {
                color: this.runtime.keyValueStyle?.textColor || this.runtime.fontColor,
                font: getDefaultContextFont(keyValueFontSize, this.runtime.keyValueStyle?.bold, this.runtime.keyValueStyle?.italic),
                strikethrough: this.runtime.keyValueStyle?.strikethrough,
                underline: this.runtime.keyValueStyle?.underline,
            },
            keyDescr: {
                color: this.runtime.keyValueDescrStyle?.textColor || this.runtime.fontColor,
                font: getDefaultContextFont(keyValueDescrFontSize, this.runtime.keyValueDescrStyle?.bold, this.runtime.keyValueDescrStyle?.italic),
                strikethrough: this.runtime.keyValueDescrStyle?.strikethrough,
                underline: this.runtime.keyValueDescrStyle?.underline,
            },
            baselineValue: {
                font: getDefaultContextFont(baselineValueFontSize, this.runtime.baselineStyle?.bold, this.runtime.baselineStyle?.italic),
                strikethrough: this.runtime.baselineStyle?.strikethrough,
                underline: this.runtime.baselineStyle?.underline,
                color: this.runtime.baselineColor ||
                    this.runtime.baselineStyle?.textColor ||
                    this.secondaryFontColor,
            },
            baselineDescr: {
                font: getDefaultContextFont(baselineDescrFontSize, this.runtime.baselineDescrStyle?.bold, this.runtime.baselineDescrStyle?.italic),
                strikethrough: this.runtime.baselineDescrStyle?.strikethrough,
                underline: this.runtime.baselineDescrStyle?.underline,
                color: this.runtime.baselineDescrStyle?.textColor ?? this.secondaryFontColor,
            },
            baselineArrow: this.baselineArrow === "neutral" || this.runtime.progressBar
                ? undefined
                : {
                    size: this.keyValue ? 0.8 * baselineValueFontSize : 0,
                    color: this.runtime.baselineColor ||
                        this.runtime.baselineStyle?.textColor ||
                        this.secondaryFontColor,
                },
        };
    }
}

class ScorecardChart extends Component {
    static template = "o-spreadsheet-ScorecardChart";
    static props = {
        chartId: String,
        isFullScreen: { type: Boolean, optional: true },
    };
    canvas = useRef("chartContainer");
    get runtime() {
        return this.env.model.getters.getChartRuntime(this.props.chartId);
    }
    get title() {
        const title = this.env.model.getters.getChartDefinition(this.props.chartId).title.text;
        return title ? this.env.model.getters.dynamicTranslate(title) : "";
    }
    setup() {
        useEffect(this.createChart.bind(this), () => {
            const canvas = this.canvas.el;
            const rect = canvas.getBoundingClientRect();
            return [rect.width, rect.height, this.runtime, this.canvas.el, window.devicePixelRatio];
        });
    }
    createChart() {
        const canvas = this.canvas.el;
        const config = getScorecardConfiguration(canvas.getBoundingClientRect(), this.runtime);
        drawScoreChart(config, canvas);
    }
}

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) {
    if (!parent)
        return false;
    return !!ev.target && parent.contains(ev.target);
}
function gridOverlayPosition() {
    const spreadsheetElement = document.querySelector(".o-grid-overlay");
    if (spreadsheetElement) {
        const { top, left } = spreadsheetElement.getBoundingClientRect();
        return { top, left };
    }
    throw new Error("Can't find spreadsheet position");
}
function getRefBoundingRect(ref) {
    if (!ref.el) {
        return { x: 0, y: 0, width: 0, height: 0 };
    }
    return getBoundingRectAsPOJO(ref.el);
}
function getBoundingRectAsPOJO(el) {
    const rect = el.getBoundingClientRect();
    return {
        x: rect.x,
        y: rect.y,
        width: rect.width,
        height: rect.height,
    };
}
/**
 * Iterate over all the children of `el` in the dom tree starting at `el`, depth first.
 */
function* iterateChildren(el) {
    yield el;
    if (el.hasChildNodes()) {
        for (const child of el.childNodes) {
            yield* iterateChildren(child);
        }
    }
}
function getOpenedMenus() {
    return Array.from(document.querySelectorAll(".o-spreadsheet .o-menu"));
}
function getCurrentSelection(el) {
    const { startElement, endElement, startSelectionOffset, endSelectionOffset } = getStartAndEndSelection(el);
    const startSizeBefore = findSelectionIndex(el, startElement, startSelectionOffset);
    const endSizeBefore = findSelectionIndex(el, endElement, endSelectionOffset);
    return {
        start: startSizeBefore,
        end: endSizeBefore,
    };
}
function getStartAndEndSelection(el) {
    const selection = document.getSelection();
    return {
        startElement: selection.anchorNode || el,
        startSelectionOffset: selection.anchorOffset,
        endElement: selection.focusNode || el,
        endSelectionOffset: selection.focusOffset,
    };
}
/**
 * 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 nodes 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
 *
 */
function findSelectionIndex(el, nodeToFind, nodeOffset) {
    let usedCharacters = 0;
    const it = iterateChildren(el);
    let current = it.next();
    let isFirstParagraph = true;
    while (!current.done && current.value !== nodeToFind) {
        if (!current.value.hasChildNodes()) {
            if (current.value.textContent) {
                usedCharacters += current.value.textContent.length;
            }
        }
        // One new paragraph = one new line character, except for the first paragraph
        if (current.value.nodeName === "P" ||
            (current.value.nodeName === "DIV" && current.value !== el) // On paste, the HTML may contain <div> instead of <p>
        ) {
            if (isFirstParagraph) {
                isFirstParagraph = false;
            }
            else {
                usedCharacters++;
            }
        }
        current = it.next();
    }
    if (current.value !== nodeToFind) {
        /** This situation can happen if the code is called while the selection is not currently on the element.
         * In this case, we return 0 because we don't know the size of the text before the selection.
         *
         * A known occurrence is triggered since the introduction of commit d4663158 (PR #2038).
         */
        return 0;
    }
    else {
        if (!current.value.hasChildNodes()) {
            usedCharacters += nodeOffset;
        }
        else {
            const children = [...current.value.childNodes].slice(0, nodeOffset);
            usedCharacters += children.reduce((acc, child, index) => {
                if (child.textContent !== null) {
                    // need to account for paragraph nodes that implicitly add a new line
                    // except for the last paragraph
                    let chars = child.textContent.length;
                    if (child.nodeName === "P" && index !== children.length - 1) {
                        chars++;
                    }
                    return acc + chars;
                }
                else {
                    return acc;
                }
            }, 0);
        }
    }
    if (nodeToFind.nodeName === "P" && !isFirstParagraph && nodeToFind.textContent === "") {
        usedCharacters++;
    }
    return usedCharacters;
}
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() || isIOS() ? ev.metaKey : ev.ctrlKey;
}
/**
 * @param {MouseEvent} ev - The mouse event.
 * @returns {boolean} Returns true if the event was triggered by a middle-click
 * or a Ctrl + Click (Cmd + Click on Mac).
 */
function isMiddleClickOrCtrlClick(ev) {
    return ev.button === 1 || (isCtrlKey(ev) && ev.button === 0);
}
async function convertImageToPng(imageUrl) {
    return new Promise((resolve, reject) => {
        const image = new Image();
        image.addEventListener("load", () => {
            const canvas = document.createElement("canvas");
            canvas.width = image.width;
            canvas.height = image.height;
            const ctx = canvas.getContext("2d");
            ctx?.drawImage(image, 0, 0);
            canvas.toBlob(resolve, "image/png");
        });
        image.addEventListener("error", reject);
        image.src = imageUrl;
    });
}
function downloadFile(dataUrl, fileName) {
    const a = document.createElement("a");
    a.href = dataUrl;
    a.download = fileName;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
}
/**
 * Detects if the current browser is Firefox
 */
function isBrowserFirefox() {
    return /Firefox/i.test(navigator.userAgent);
}
// Mobile detection
function maxTouchPoints() {
    return navigator.maxTouchPoints || 1;
}
function isAndroid() {
    return /Android/i.test(navigator.userAgent);
}
function isIOS() {
    return (/(iPad|iPhone|iPod)/i.test(navigator.userAgent) ||
        (navigator.platform === "MacIntel" && maxTouchPoints() > 1));
}
function isOtherMobileOS() {
    return /(webOS|BlackBerry|Windows Phone)/i.test(navigator.userAgent);
}
function isMobileOS() {
    return isAndroid() || isIOS() || isOtherMobileOS();
}

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

const GAUGE_PADDING_SIDE = 30;
const GAUGE_PADDING_TOP = 10;
const GAUGE_PADDING_BOTTOM = 20;
const GAUGE_LABELS_FONT_SIZE = 12;
const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
const GAUGE_TITLE_SECTION_HEIGHT = 25;
function drawGaugeChart(canvas, runtime) {
    const canvasBoundingRect = canvas.getBoundingClientRect();
    const dpr = window.devicePixelRatio || 1;
    canvas.width = dpr * canvasBoundingRect.width;
    canvas.height = dpr * canvasBoundingRect.height;
    const ctx = canvas.getContext("2d");
    ctx.scale(dpr, dpr);
    const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
    drawBackground(ctx, config);
    drawGauge(ctx, config);
    drawInflectionValues(ctx, config);
    drawLabels(ctx, config);
    drawTitle(ctx, config);
}
function drawGauge(ctx, config) {
    ctx.save();
    const gauge = config.gauge;
    const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
    const arcCenterY = gauge.rect.y + gauge.rect.height;
    const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
    if (arcRadius < 0) {
        return;
    }
    const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
    // Gauge background
    ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
    ctx.beginPath();
    ctx.lineWidth = gauge.arcWidth;
    ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
    ctx.stroke();
    // Gauge value
    ctx.strokeStyle = gauge.color;
    ctx.beginPath();
    ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
    ctx.stroke();
    ctx.restore();
}
function drawBackground(ctx, config) {
    ctx.save();
    ctx.fillStyle = config.backgroundColor;
    ctx.fillRect(0, 0, config.width, config.height);
    ctx.restore();
}
function drawLabels(ctx, config) {
    for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
        ctx.save();
        ctx.textAlign = "center";
        ctx.fillStyle = label.color;
        ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
        ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
        ctx.restore();
    }
}
function drawInflectionValues(ctx, config) {
    const { x: rectX, y: rectY, width, height } = config.gauge.rect;
    for (const inflectionValue of config.inflectionValues) {
        ctx.save();
        ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
        ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
        ctx.lineWidth = 2;
        ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
        ctx.beginPath();
        ctx.moveTo(0, -(height - config.gauge.arcWidth));
        ctx.lineTo(0, -height - 3);
        ctx.stroke();
        ctx.textAlign = "center";
        ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
        ctx.fillStyle = inflectionValue.color;
        const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
        ctx.fillText(inflectionValue.label, 0, textY);
        ctx.restore();
    }
}
function drawTitle(ctx, config) {
    ctx.save();
    const title = config.title;
    ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
    ctx.textBaseline = "middle";
    ctx.fillStyle = title.color;
    ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
    ctx.restore();
}
function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
    const maxValue = runtime.maxValue;
    const minValue = runtime.minValue;
    const gaugeValue = getGaugeValue(runtime, "animated");
    const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
    const gaugeArcWidth = gaugeRect.width / 6;
    const gaugePercentage = gaugeValue
        ? (gaugeValue - minValue.value) / (maxValue.value - minValue.value)
        : 0;
    const gaugeValuePosition = {
        x: boundingRect.width / 2,
        y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
    };
    let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
    // Scale down the font size if the gaugeRect is too small
    if (gaugeRect.height < 300) {
        gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
    }
    // Scale down the font size if the text is too long
    const maxTextWidth = gaugeRect.width / 2;
    const gaugeLabel = runtime.gaugeValue?.label || "-";
    if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
        gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
    }
    const minLabelPosition = {
        x: gaugeRect.x + gaugeArcWidth / 2,
        y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
    };
    const maxLabelPosition = {
        x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
        y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
    };
    const textColor = chartMutedFontColor(runtime.background);
    const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
    let x = 0, titleWidth = 0, titleHeight = 0;
    if (runtime.title.text) {
        ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
    }
    switch (runtime.title.align) {
        case "right":
            x = boundingRect.width - titleWidth - CHART_PADDING;
            break;
        case "center":
            x = (boundingRect.width - titleWidth) / 2;
            break;
        case "left":
        default:
            x = CHART_PADDING;
            break;
    }
    return {
        width: boundingRect.width,
        height: boundingRect.height,
        title: {
            label: runtime.title.text ?? "",
            fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
            textPosition: {
                x,
                y: CHART_PADDING_TOP + titleHeight / 2,
            },
            color: runtime.title.color ?? textColor,
            bold: runtime.title.bold,
            italic: runtime.title.italic,
        },
        backgroundColor: runtime.background,
        gauge: {
            rect: gaugeRect,
            arcWidth: gaugeArcWidth,
            percentage: clip(gaugePercentage, 0, 1),
            color: getGaugeColor(runtime),
        },
        inflectionValues,
        gaugeValue: {
            label: gaugeLabel,
            textPosition: gaugeValuePosition,
            fontSize: gaugeValueFontSize,
            color: textColor,
        },
        minLabel: {
            label: runtime.minValue.label,
            textPosition: minLabelPosition,
            fontSize: GAUGE_LABELS_FONT_SIZE,
            color: textColor,
        },
        maxLabel: {
            label: runtime.maxValue.label,
            textPosition: maxLabelPosition,
            fontSize: GAUGE_LABELS_FONT_SIZE,
            color: textColor,
        },
    };
}
/**
 * Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
 * space for the title and labels.
 */
function getGaugeRect(boundingRect, title) {
    const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
    const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
    const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
    let gaugeWidth;
    let gaugeHeight;
    if (drawWidth > 2 * drawHeight) {
        gaugeWidth = 2 * drawHeight;
        gaugeHeight = drawHeight;
    }
    else {
        gaugeWidth = drawWidth;
        gaugeHeight = drawWidth / 2;
    }
    const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
    const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
    return {
        x: gaugeX,
        y: gaugeY,
        width: gaugeWidth,
        height: gaugeHeight,
    };
}
/**
 * Get the infliction values of the gauge, and where to draw them (the angle from the center of the gauge at which they are drawn).
 *
 * Also compute an offset for the text so that it doesn't overlap with other text.
 */
function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
    const maxValue = runtime.maxValue;
    const minValue = runtime.minValue;
    const gaugeCircleCenter = {
        x: gaugeRect.x + gaugeRect.width / 2,
        y: gaugeRect.y + gaugeRect.height,
    };
    const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
    const inflectionValues = [];
    const inflectionValuesTextRects = [];
    for (const inflectionValue of runtime.inflectionValues) {
        const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
        const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
        const angle = Math.PI - Math.PI * percentage;
        const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
        gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
        gaugeCircleCenter.x, // center of the gauge circle
        gaugeCircleCenter.y, // center of the gauge circle
        labelWidth + 2, // width of the text + some margin
        GAUGE_LABELS_FONT_SIZE // height of the text
        );
        const offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
            ? GAUGE_LABELS_FONT_SIZE
            : 0;
        inflectionValuesTextRects.push(textRect);
        inflectionValues.push({
            rotation: angle,
            label: inflectionValue.label,
            fontSize: GAUGE_LABELS_FONT_SIZE,
            color: textColor,
            offset,
        });
    }
    return inflectionValues;
}
function getGaugeColor(runtime) {
    const gaugeValue = getGaugeValue(runtime, "final");
    if (gaugeValue === undefined) {
        return GAUGE_BACKGROUND_COLOR;
    }
    for (let i = 0; i < runtime.inflectionValues.length; i++) {
        const inflectionValue = runtime.inflectionValues[i];
        if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
            return runtime.colors[i];
        }
        else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
            return runtime.colors[i];
        }
    }
    return runtime.colors.at(-1);
}
function getSegmentsOfRectangle(rectangle) {
    return [
        { start: rectangle.topLeft, end: rectangle.topRight },
        { start: rectangle.topRight, end: rectangle.bottomRight },
        { start: rectangle.bottomRight, end: rectangle.bottomLeft },
        { start: rectangle.bottomLeft, end: rectangle.topLeft },
    ];
}
/**
 * Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
 * is not handled.
 */
function doSegmentIntersect(segment1, segment2) {
    const A = segment1.start;
    const B = segment1.end;
    const C = segment2.start;
    const D = segment2.end;
    /**
     * Line segment intersection algorithm
     * https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
     */
    function ccw(a, b, c) {
        return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
    }
    return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
}
function doRectanglesIntersect(rect1, rect2) {
    const segments1 = getSegmentsOfRectangle(rect1);
    const segments2 = getSegmentsOfRectangle(rect2);
    for (const segment1 of segments1) {
        for (const segment2 of segments2) {
            if (doSegmentIntersect(segment1, segment2)) {
                return true;
            }
        }
    }
    return false;
}
/**
 *  Get the rectangle that is tangent to a circle at a given angle.
 *
 * @param angle angle between X axis and the point where the rectangle is tangent to the circle
 */
function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
    const cos = Math.cos(angle);
    const sin = Math.sin(angle);
    // x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
    const x = cos * radius;
    const y = sin * radius;
    // x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
    const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
    const y2 = cos * (rectWidth / 2);
    const bottomRight = {
        x: x + x2 + circleCenterX,
        y: circleCenterY - (y - y2),
    };
    const bottomLeft = {
        x: x - x2 + circleCenterX,
        y: circleCenterY - (y + y2),
    };
    // Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
    const xp = cos * (radius + rectHeight);
    const yp = sin * (radius + rectHeight);
    const topLeft = {
        x: xp - x2 + circleCenterX,
        y: circleCenterY - (yp + y2),
    };
    const topRight = {
        x: xp + x2 + circleCenterX,
        y: circleCenterY - (yp - y2),
    };
    return { bottomLeft, bottomRight, topRight, topLeft };
}
function getGaugeValue(runtime, mode) {
    return mode === "animated" && runtime.animationValue !== undefined
        ? runtime.animationValue
        : runtime.gaugeValue?.value;
}

const CHART_COMMON_OPTIONS = {
    // https://www.chartjs.org/docs/latest/general/responsive.html
    responsive: true, // will resize when its container is resized
    maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
    elements: {
        line: {
            fill: false, // do not fill the area under line charts
        },
        point: {
            hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
        },
    },
    animation: false,
    events: ["mousemove", "mouseout", "click", "touchstart", "touchmove", "mouseup"],
};
function chartToImageUrl(runtime, figure, type) {
    // wrap the canvas in a div with a fixed size because chart.js would
    // fill the whole page otherwise
    const div = document.createElement("div");
    div.style.width = `${figure.width}px`;
    div.style.height = `${figure.height}px`;
    const canvas = document.createElement("canvas");
    div.append(canvas);
    canvas.setAttribute("width", figure.width.toString());
    canvas.setAttribute("height", figure.height.toString());
    let imageContent;
    // we have to add the canvas to the DOM otherwise it won't be rendered
    document.body.append(div);
    if ("chartJsConfig" in runtime) {
        const extensionsLoaded = areChartJSExtensionsLoaded();
        if (!extensionsLoaded) {
            registerChartJSExtensions();
        }
        const config = deepCopy(runtime.chartJsConfig);
        config.plugins = [backgroundColorChartJSPlugin];
        const chart = new window.Chart(canvas, config);
        imageContent = chart.toBase64Image();
        chart.destroy();
        if (!extensionsLoaded) {
            unregisterChartJsExtensions();
        }
    }
    else if (type === "scorecard") {
        const design = getScorecardConfiguration(figure, runtime);
        drawScoreChart(design, canvas);
        imageContent = canvas.toDataURL();
    }
    else if (type === "gauge") {
        drawGaugeChart(canvas, runtime);
        imageContent = canvas.toDataURL();
    }
    div.remove();
    return imageContent;
}
async function chartToImageFile(runtime, figure, type) {
    // wrap the canvas in a div with a fixed size because chart.js would
    // fill the whole page otherwise
    const div = document.createElement("div");
    div.style.width = `${figure.width}px`;
    div.style.height = `${figure.height}px`;
    div.style.position = "fixed";
    div.style.opacity = "0";
    const canvas = document.createElement("canvas");
    div.append(canvas);
    canvas.setAttribute("width", figure.width.toString());
    canvas.setAttribute("height", figure.height.toString());
    // we have to add the canvas to the DOM otherwise it won't be rendered
    document.body.append(div);
    let chartBlob = null;
    if ("chartJsConfig" in runtime) {
        const extensionsLoaded = areChartJSExtensionsLoaded();
        if (!extensionsLoaded) {
            registerChartJSExtensions();
        }
        const config = deepCopy(runtime.chartJsConfig);
        config.plugins = [backgroundColorChartJSPlugin];
        const chart = new window.Chart(canvas, config);
        chartBlob = await new Promise((resolve) => canvas.toBlob(resolve, "image/png"));
        chart.destroy();
        if (!extensionsLoaded) {
            unregisterChartJsExtensions();
        }
    }
    else if (type === "scorecard") {
        const design = getScorecardConfiguration(figure, runtime);
        drawScoreChart(design, canvas);
        chartBlob = await new Promise((resolve) => canvas.toBlob(resolve, "image/png"));
    }
    else if (type === "gauge") {
        drawGaugeChart(canvas, runtime);
        chartBlob = await new Promise((resolve) => canvas.toBlob(resolve, "image/png"));
    }
    div.remove();
    return chartBlob ? new File([chartBlob], "chart.png", { type: "image/png" }) : undefined;
}
/**
 * Custom chart.js plugin to set the background color of the canvas
 * https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
 */
const backgroundColorChartJSPlugin = {
    id: "customCanvasBackgroundColor",
    beforeDraw: (chart) => {
        const { ctx } = chart;
        ctx.save();
        ctx.globalCompositeOperation = "destination-over";
        ctx.fillStyle = "#ffffff";
        ctx.fillRect(0, 0, chart.width, chart.height);
        ctx.restore();
    },
};

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

function getBarChartData(definition, dataSets, labelRange, getters) {
    const labelValues = getChartLabelValues(getters, dataSets, labelRange);
    let labels = labelValues.formattedValues;
    let dataSetsValues = getChartDatasetValues(getters, dataSets);
    if (shouldRemoveFirstLabel(labelRange, dataSets[0], definition.dataSetsHaveTitle || false)) {
        labels.shift();
    }
    ({ labels, dataSetsValues } = filterInvalidDataPoints(labels, dataSetsValues));
    if (definition.aggregated) {
        ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
    }
    const leftAxisFormat = getChartDatasetFormat(getters, dataSets, "left");
    const rightAxisFormat = getChartDatasetFormat(getters, dataSets, "right");
    const axisFormats = definition.horizontal
        ? { x: leftAxisFormat || rightAxisFormat }
        : { y: leftAxisFormat, y1: rightAxisFormat };
    const trendDataSetsValues = [];
    for (const index in dataSetsValues) {
        const { data } = dataSetsValues[index];
        const trend = definition.dataSets?.[index].trend;
        if (!trend?.display || definition.horizontal) {
            trendDataSetsValues.push(undefined);
            continue;
        }
        const trendDataset = getTrendDatasetForBarChart(trend, data);
        trendDataSetsValues.push(trendDataset);
    }
    return {
        dataSetsValues,
        trendDataSetsValues,
        axisFormats,
        labels,
        locale: getters.getLocale(),
        topPadding: getTopPaddingForDashboard(definition, getters),
    };
}
function getPyramidChartData(definition, dataSets, labelRange, getters) {
    const barChartData = getBarChartData(definition, dataSets.slice(0, 2), labelRange, getters);
    const barDataset = barChartData.dataSetsValues.filter((ds) => !ds.hidden);
    const pyramidDatasetValues = [];
    if (barDataset[0]) {
        const pyramidData = barDataset[0].data.map((value) => (value > 0 ? value : 0));
        pyramidDatasetValues.push({ ...barDataset[0], data: pyramidData });
    }
    if (barDataset[1]) {
        const pyramidData = barDataset[1].data.map((value) => (value > 0 ? -value : 0));
        pyramidDatasetValues.push({ ...barDataset[1], data: pyramidData });
    }
    return {
        ...barChartData,
        dataSetsValues: pyramidDatasetValues,
    };
}
function getLineChartData(definition, dataSets, labelRange, getters) {
    const axisType = getChartAxisType(definition, dataSets, labelRange, getters);
    const labelValues = getChartLabelValues(getters, dataSets, labelRange);
    let labels = axisType === "linear" ? labelValues.values : labelValues.formattedValues;
    let dataSetsValues = getChartDatasetValues(getters, dataSets);
    const removeFirstLabel = shouldRemoveFirstLabel(labelRange, dataSets[0], definition.dataSetsHaveTitle || false);
    if (removeFirstLabel) {
        labels.shift();
    }
    ({ labels, dataSetsValues } = filterInvalidDataPoints(labels, dataSetsValues));
    if (axisType === "time") {
        ({ labels, dataSetsValues } = fixEmptyLabelsForDateCharts(labels, dataSetsValues));
    }
    if (definition.aggregated) {
        ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
    }
    if (definition.cumulative) {
        dataSetsValues = makeDatasetsCumulative(dataSetsValues, "asc");
    }
    const leftAxisFormat = getChartDatasetFormat(getters, dataSets, "left");
    const rightAxisFormat = getChartDatasetFormat(getters, dataSets, "right");
    const labelsFormat = getChartLabelFormat(getters, labelRange, removeFirstLabel);
    const axisFormats = { y: leftAxisFormat, y1: rightAxisFormat, x: labelsFormat };
    const trendDataSetsValues = [];
    for (const index in dataSetsValues) {
        const trend = definition.dataSets?.[index].trend;
        if (!trend?.display) {
            trendDataSetsValues.push(undefined);
            continue;
        }
        const { data } = dataSetsValues[index];
        trendDataSetsValues.push(getTrendDatasetForLineChart(trend, data, labels, axisType, getters.getLocale()));
    }
    return {
        dataSetsValues,
        axisFormats,
        labels,
        locale: getters.getLocale(),
        trendDataSetsValues,
        axisType,
        topPadding: getTopPaddingForDashboard(definition, getters),
    };
}
function getPieChartData(definition, dataSets, labelRange, getters) {
    const labelValues = getChartLabelValues(getters, dataSets, labelRange);
    let labels = labelValues.formattedValues;
    let dataSetsValues = getChartDatasetValues(getters, dataSets);
    if (shouldRemoveFirstLabel(labelRange, dataSets[0], definition.dataSetsHaveTitle || false)) {
        labels.shift();
    }
    ({ labels, dataSetsValues } = filterInvalidDataPoints(labels, dataSetsValues));
    if (definition.aggregated) {
        ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
    }
    ({ dataSetsValues, labels } = keepOnlyPositiveValues(labels, dataSetsValues));
    const dataSetFormat = getChartDatasetFormat(getters, dataSets, "left");
    return {
        dataSetsValues,
        axisFormats: { y: dataSetFormat },
        labels,
        locale: getters.getLocale(),
        topPadding: getTopPaddingForDashboard(definition, getters),
    };
}
function getRadarChartData(definition, dataSets, labelRange, getters) {
    const labelValues = getChartLabelValues(getters, dataSets, labelRange);
    let labels = labelValues.formattedValues;
    let dataSetsValues = getChartDatasetValues(getters, dataSets);
    if (shouldRemoveFirstLabel(labelRange, dataSets[0], definition.dataSetsHaveTitle || false)) {
        labels.shift();
    }
    ({ labels, dataSetsValues } = filterInvalidDataPoints(labels, dataSetsValues));
    if (definition.aggregated) {
        ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
    }
    const dataSetFormat = getChartDatasetFormat(getters, dataSets, "left") ||
        getChartDatasetFormat(getters, dataSets, "right");
    const axisFormats = { r: dataSetFormat };
    return {
        dataSetsValues,
        axisFormats,
        labels,
        locale: getters.getLocale(),
    };
}
function getGeoChartData(definition, fullDataSets, labelRange, getters) {
    const dataSets = fullDataSets.slice(0, 1);
    const labelValues = getChartLabelValues(getters, dataSets, labelRange);
    let labels = labelValues.formattedValues;
    if (shouldRemoveFirstLabel(labelRange, dataSets[0], definition.dataSetsHaveTitle || false)) {
        labels.shift();
    }
    let dataSetsValues = getChartDatasetValues(getters, dataSets);
    ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
    const format = getChartDatasetFormat(getters, dataSets, "left") ||
        getChartDatasetFormat(getters, dataSets, "right");
    return {
        dataSetsValues,
        axisFormats: { y: format },
        labels,
        locale: getters.getLocale(),
        availableRegions: getters.getGeoChartAvailableRegions(),
        geoFeatureNameToId: getters.geoFeatureNameToId,
        getGeoJsonFeatures: getters.getGeoJsonFeatures,
    };
}
function getFunnelChartData(definition, dataSets, labelRange, getters) {
    const labelValues = getChartLabelValues(getters, dataSets, labelRange);
    let labels = labelValues.formattedValues;
    let dataSetsValues = getChartDatasetValues(getters, dataSets);
    if (shouldRemoveFirstLabel(labelRange, dataSets[0], definition.dataSetsHaveTitle || false)) {
        labels.shift();
    }
    ({ labels, dataSetsValues } = filterInvalidDataPoints(labels, dataSetsValues));
    if (definition.aggregated) {
        ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
    }
    if (definition.cumulative) {
        dataSetsValues = makeDatasetsCumulative(dataSetsValues, "desc");
    }
    const format = getChartDatasetFormat(getters, dataSets, "left") ||
        getChartDatasetFormat(getters, dataSets, "right");
    return {
        dataSetsValues,
        axisFormats: { x: format },
        labels,
        locale: getters.getLocale(),
    };
}
function getHierarchalChartData(definition, dataSets, labelRange, getters) {
    // In hierarchical charts, labels are the leaf values (numbers), and the hierarchy is defined in the dataSets (strings)
    let labels = getChartLabelValues(getters, dataSets, labelRange).values;
    let dataSetsValues = getHierarchicalDatasetValues(getters, dataSets);
    const removeFirstLabel = shouldRemoveFirstLabel(labelRange, dataSets[0], definition.dataSetsHaveTitle || false);
    if (removeFirstLabel) {
        labels.shift();
    }
    ({ labels, dataSetsValues } = filterValuesWithDifferentSigns(labels, dataSetsValues));
    ({ labels, dataSetsValues } = filterInvalidHierarchicalPoints(labels, dataSetsValues));
    return {
        dataSetsValues,
        axisFormats: { y: getChartLabelFormat(getters, labelRange, removeFirstLabel) },
        labels,
        locale: getters.getLocale(),
    };
}
function getTrendDatasetForBarChart(config, data) {
    const filteredValues = [];
    const filteredLabels = [];
    const labels = [];
    for (let i = 0; i < data.length; i++) {
        if (typeof data[i] === "number") {
            filteredValues.push(data[i]);
            filteredLabels.push(i + 1);
        }
        labels.push(i + 1);
    }
    const newLabels = range(0.5, labels.length + 0.55, 0.2);
    const newValues = interpolateData(config, filteredValues, filteredLabels, newLabels);
    return newValues.length ? newValues : undefined;
}
function getTrendDatasetForLineChart(config, data, labels, axisType, locale) {
    const filteredValues = [];
    const filteredLabels = [];
    const trendLabels = [];
    const datasetLength = data.length;
    if (datasetLength < 2) {
        return;
    }
    switch (axisType) {
        case "category":
            for (let i = 0; i < datasetLength; i++) {
                if (typeof data[i] === "number") {
                    filteredValues.push(data[i]);
                    filteredLabels.push(i + 1);
                }
                trendLabels.push(i + 1);
            }
            break;
        case "linear":
            for (let i = 0; i < data.length; i++) {
                const label = Number(labels[i]);
                if (isNaN(label)) {
                    continue;
                }
                if (typeof data[i] === "number") {
                    filteredValues.push(data[i]);
                    filteredLabels.push(label);
                }
                trendLabels.push(label);
            }
            break;
        case "time":
            for (let i = 0; i < data.length; i++) {
                const date = toNumber({ value: labels[i] }, locale);
                if (data[i] !== null) {
                    filteredValues.push(data[i]);
                    filteredLabels.push(date);
                }
                trendLabels.push(date);
            }
            break;
    }
    const xmin = Math.min(...trendLabels);
    const xmax = Math.max(...trendLabels);
    if (xmax === xmin) {
        return;
    }
    const numberOfStep = 5 * trendLabels.length;
    const step = (xmax - xmin) / numberOfStep;
    const trendNewLabels = range(xmin, xmax + step / 2, step);
    const trendValues = interpolateData(config, filteredValues, filteredLabels, trendNewLabels);
    if (!trendValues.length) {
        return;
    }
    return trendValues;
}
function interpolateData(config, values, labels, newLabels) {
    if (values.length < 2 || labels.length < 2 || newLabels.length === 0) {
        return [];
    }
    const { normalizedLabels, normalizedNewLabels } = normalizeLabels(labels, newLabels, config);
    try {
        switch (config.type) {
            case "polynomial": {
                const order = config.order;
                if (!order) {
                    return newLabels.map((x) => ({ x, y: NaN }));
                }
                if (order === 1) {
                    return predictLinearValues([values], [normalizedLabels], [normalizedNewLabels], true)[0].map((y, i) => ({ x: newLabels[i], y }));
                }
                const coeffs = polynomialRegression(values, normalizedLabels, order, true).flat();
                return normalizedNewLabels.map((x, i) => ({
                    x: newLabels[i],
                    y: evaluatePolynomial(coeffs, x, order),
                }));
            }
            case "exponential": {
                const positiveLogValues = [];
                const filteredLabels = [];
                for (let i = 0; i < values.length; i++) {
                    if (values[i] > 0) {
                        positiveLogValues.push(Math.log(values[i]));
                        filteredLabels.push(normalizedLabels[i]);
                    }
                }
                if (!filteredLabels.length) {
                    return newLabels.map((x) => ({ x, y: NaN }));
                }
                return expM(predictLinearValues([positiveLogValues], [filteredLabels], [normalizedNewLabels], true))[0].map((y, i) => ({ x: newLabels[i], y }));
            }
            case "logarithmic": {
                return predictLinearValues([values], logM([normalizedLabels]), logM([normalizedNewLabels]), true)[0].map((y, i) => ({ x: newLabels[i], y }));
            }
            case "trailingMovingAverage": {
                return getMovingAverageValues(values, labels, config.window);
            }
            default:
                return newLabels.map((x) => ({ x, y: NaN }));
        }
    }
    catch (e) {
        return newLabels.map((x) => ({ x, y: NaN }));
    }
}
function normalizeLabels(labels, newLabels, config) {
    let normalizedLabels = [];
    let normalizedNewLabels = [];
    if (config.type === "logarithmic") {
        // Logarithmic trends in charts are used to visualize proportional growth or
        // relative changes. Therefore, we change the normalization technique for
        // logarithmic trend lines for a better fit. The method used here is Max Absolute
        // Scaling. This Technique is ideal for data spanning several orders of magnitude,
        // as it balances differences between small and large values by compressing larger
        // values while preserving proportionality and ensuring all values are scaled relative
        // to the largest magnitude.
        const labelMax = Math.max(...labels.map(Math.abs));
        normalizedLabels = labels.map((l) => l / labelMax);
        normalizedNewLabels = newLabels.map((l) => l / labelMax);
    }
    else {
        const labelMax = Math.max(...labels);
        const labelMin = Math.min(...labels);
        const labelRange = labelMax - labelMin;
        normalizedLabels = labels.map((l) => (l - labelMax) / labelRange);
        normalizedNewLabels = newLabels.map((l) => (l - labelMax) / labelRange);
    }
    return { normalizedLabels, normalizedNewLabels };
}
function getChartAxisType(definition, dataSets, labelRange, getters) {
    if (isDateChart(definition, dataSets, labelRange, getters) && isLuxonTimeAdapterInstalled()) {
        return "time";
    }
    if (isLinearChart(definition, dataSets, labelRange, getters)) {
        return "linear";
    }
    return "category";
}
function isDateChart(definition, dataSets, labelRange, getters) {
    return !definition.labelsAsText && canBeDateChart(definition, dataSets, labelRange, getters);
}
function isLinearChart(definition, dataSets, labelRange, getters) {
    return !definition.labelsAsText && canBeLinearChart(definition, dataSets, labelRange, getters);
}
function canChartParseLabels(definition, dataSets, labelRange, getters) {
    return (canBeDateChart(definition, dataSets, labelRange, getters) ||
        canBeLinearChart(definition, dataSets, labelRange, getters));
}
function canBeDateChart(definition, dataSets, labelRange, getters) {
    if (!labelRange || !canBeLinearChart(definition, dataSets, labelRange, getters)) {
        return false;
    }
    const removeFirstLabel = shouldRemoveFirstLabel(labelRange, dataSets[0], definition.dataSetsHaveTitle || false);
    const labelFormat = getChartLabelFormat(getters, labelRange, removeFirstLabel);
    return Boolean(labelFormat && timeFormatLuxonCompatible.test(labelFormat));
}
function canBeLinearChart(definition, dataSets, labelRange, getters) {
    if (!labelRange) {
        return false;
    }
    const labels = getters.getRangeValues(labelRange);
    if (shouldRemoveFirstLabel(labelRange, dataSets[0], definition.dataSetsHaveTitle || false)) {
        labels.shift();
    }
    if (labels.some((label) => isNaN(Number(label)) && label)) {
        return false;
    }
    if (labels.every((label) => !label)) {
        return false;
    }
    return true;
}
let missingTimeAdapterAlreadyWarned = false;
function isLuxonTimeAdapterInstalled() {
    if (!window.Chart) {
        return false;
    }
    const adapter = new window.Chart._adapters._date({});
    // @ts-ignore
    const isInstalled = adapter._id === "luxon";
    if (!isInstalled && !missingTimeAdapterAlreadyWarned) {
        missingTimeAdapterAlreadyWarned = true;
        console.warn("'chartjs-adapter-luxon' time adapter is not installed. Time scale axes are disabled.");
    }
    return isInstalled;
}
function keepOnlyPositiveValues(labels, datasets) {
    const numberOfDataPoints = Math.max(labels.length, ...datasets.map((dataset) => dataset.data?.length || 0));
    const filteredIndexes = range(0, numberOfDataPoints).filter((i) => datasets.some((ds) => typeof ds.data[i] === "number" && ds.data[i] > 0));
    return {
        labels: filteredIndexes.map((i) => labels[i] || ""),
        dataSetsValues: datasets.map((ds) => ({
            ...ds,
            data: filteredIndexes.map((i) => typeof ds.data[i] === "number" && ds.data[i] > 0 ? ds.data[i] : null),
        })),
    };
}
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 (const ds of newDatasets) {
                ds.data[i] = undefined;
            }
        }
    }
    return { labels: newLabels, dataSetsValues: newDatasets };
}
/**
 * Get the data from a dataSet
 */
function getData(getters, ds) {
    if (ds.dataRange) {
        const labelCellZone = ds.labelCell ? [ds.labelCell.zone] : [];
        const dataZone = recomputeZones([ds.dataRange.zone], labelCellZone)[0];
        if (dataZone === undefined) {
            return [];
        }
        const dataRange = getters.getRangeFromZone(ds.dataRange.sheetId, dataZone);
        return getters.getRangeValues(dataRange).map((value) => (value === "" ? undefined : value));
    }
    return [];
}
/**
 * Filter the data points that:
 * - have neither a label nor a value
 * - have no label and a non-numeric value
 */
function filterInvalidDataPoints(labels, datasets) {
    const numberOfDataPoints = Math.max(labels.length, ...datasets.map((dataset) => dataset.data?.length || 0));
    const dataPointsIndexes = range(0, numberOfDataPoints).filter((dataPointIndex) => {
        const label = labels[dataPointIndex];
        const values = datasets.map((dataset) => dataset.data?.[dataPointIndex]);
        return label || values.some((value) => typeof value === "number");
    });
    return {
        labels: dataPointsIndexes.map((i) => labels[i] || ""),
        dataSetsValues: datasets.map((dataset) => ({
            ...dataset,
            data: dataPointsIndexes.map((i) => typeof dataset.data[i] === "number" ? dataset.data[i] : null),
        })),
    };
}
/**
 * Filter the data points that have either no value, a negative value, no root group or null group values in the middle
 */
function filterInvalidHierarchicalPoints(values, hierarchy) {
    const numberOfDataPoints = Math.max(values.length, ...hierarchy.map((dataset) => dataset.data?.length || 0));
    const isEmpty = (value) => value === undefined || value === null || value === "";
    const dataPointsIndexes = range(0, numberOfDataPoints).filter((dataPointIndex) => {
        const groups = hierarchy.map((dataset) => dataset.data?.[dataPointIndex]);
        if (isEmpty(groups[0])) {
            return false;
        }
        // Filter points with empty group in the middle
        let hasFoundEmptyGroup = false;
        for (const group of groups) {
            hasFoundEmptyGroup ||= isEmpty(group);
            if (hasFoundEmptyGroup && !isEmpty(group)) {
                return false;
            }
        }
        return values[dataPointIndex] && !isNaN(Number(values[dataPointIndex]));
    });
    return {
        labels: dataPointsIndexes.map((i) => values[i]),
        dataSetsValues: hierarchy.map((dataset) => ({
            ...dataset,
            data: dataPointsIndexes.map((i) => dataset.data[i]),
        })),
    };
}
/**
 * If the values are a mix of positive and negative values, keep only the positive ones
 */
function filterValuesWithDifferentSigns(values, hierarchy) {
    const positivePointsIndexes = [];
    const negativePointsIndexes = [];
    for (let i = 0; i < values.length; i++) {
        if (Number(values[i]) <= 0) {
            negativePointsIndexes.push(i);
        }
        else if (Number(values[i]) > 0) {
            positivePointsIndexes.push(i);
        }
    }
    const indexesToKeep = positivePointsIndexes.length
        ? positivePointsIndexes
        : negativePointsIndexes;
    return {
        labels: indexesToKeep.map((i) => values[i]),
        dataSetsValues: hierarchy.map((dataset) => ({
            ...dataset,
            data: indexesToKeep.map((i) => dataset.data[i]),
        })),
    };
}
/**
 * Aggregates data based on labels
 */
function aggregateDataForLabels(labels, datasets) {
    const parseNumber = (value) => (typeof value === "number" ? value : 0);
    const labelSet = new Set(labels);
    const labelMap = {};
    labelSet.forEach((label) => {
        labelMap[label] = new Array(datasets.length).fill(0);
    });
    for (const indexOfLabel of range(0, labels.length)) {
        const label = labels[indexOfLabel];
        for (const indexOfDataset of range(0, datasets.length)) {
            labelMap[label][indexOfDataset] += parseNumber(datasets[indexOfDataset].data[indexOfLabel]);
        }
    }
    return {
        labels: Array.from(labelSet),
        dataSetsValues: datasets.map((dataset, indexOfDataset) => ({
            ...dataset,
            data: Array.from(labelSet).map((label) => labelMap[label][indexOfDataset]),
        })),
    };
}
function getChartLabelFormat(getters, range, shouldRemoveFirstLabel) {
    if (!range)
        return undefined;
    const { sheetId, zone } = range;
    const formats = positions(zone).map((position) => getters.getEvaluatedCell({ sheetId, ...position }).format);
    if (shouldRemoveFirstLabel) {
        formats.shift();
    }
    return formats.find((format) => format !== undefined);
}
function getChartLabelValues(getters, dataSets, labelRange) {
    let labels = { values: [], formattedValues: [] };
    if (labelRange) {
        const { left } = labelRange.zone;
        if (!labelRange.invalidXc &&
            !labelRange.invalidSheetName &&
            !getters.isColHidden(labelRange.sheetId, left)) {
            labels = {
                formattedValues: getters.getRangeFormattedValues(labelRange),
                values: getters.getRangeValues(labelRange).map((val) => String(val ?? "")),
            };
        }
        else if (dataSets[0]) {
            const ranges = getData(getters, dataSets[0]);
            labels = {
                formattedValues: range(0, ranges.length).map((r) => r.toString()),
                values: labels.formattedValues,
            };
        }
    }
    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;
}
/**
 * Get the format to apply to the the dataset values. This format is defined as the first format
 * found in the dataset ranges that isn't a date format.
 */
function getChartDatasetFormat(getters, allDataSets, axis) {
    const dataSets = allDataSets.filter((ds) => (axis === "right") === !!ds.rightYAxis);
    for (const ds of dataSets) {
        const formatsInDataset = getters.getRangeFormats(ds.dataRange);
        const format = formatsInDataset.find((f) => f !== undefined && !isDateTimeFormat(f));
        if (format)
            return format;
    }
    return undefined;
}
function getChartDatasetValues(getters, dataSets) {
    const datasetValues = [];
    for (const [dsIndex, ds] of Object.entries(dataSets)) {
        let label = `${ChartTerms.Series} ${parseInt(dsIndex) + 1}`;
        let hidden = getters.isColHidden(ds.dataRange.sheetId, ds.dataRange.zone.left);
        if (ds.labelCell) {
            const { sheetId, zone } = ds.labelCell;
            const cell = getters.getEvaluatedCell({ sheetId, col: zone.left, row: zone.top });
            if (cell) {
                label = cell.formattedValue;
            }
        }
        let data = ds.dataRange ? getData(getters, ds) : [];
        if (data.every((e) => !e || (typeof e === "string" && !isEvaluationError(e))) &&
            data.filter((e) => typeof e === "string").length > 1) {
            // Convert categorical data into counts
            data = data.map((e) => (e && !isEvaluationError(e) ? 1 : null));
        }
        else if (data.every((cell) => cell === undefined || cell === null || !isNumber(cell.toString(), DEFAULT_LOCALE))) {
            hidden = true;
        }
        datasetValues.push({ data, label, hidden });
    }
    return datasetValues;
}
/**
 * Get the values for a hierarchical dataset. The values can be defined in a tree-like structure
 * in the sheet, and this function will fill up the blanks.
 *
 * @example the following dataset:
 *
 * 2024    Q1    W1    100
 *               W2    200
 *
 * will have the same value as the dataset:
 * 2024    Q1    W1    100
 * 2024    Q1    W2    200
 */
function getHierarchicalDatasetValues(getters, dataSets) {
    dataSets = dataSets.filter((ds) => !getters.isColHidden(ds.dataRange.sheetId, ds.dataRange.zone.left));
    const datasetValues = dataSets.map(() => ({ data: [], label: "" }));
    const dataSetsData = dataSets.map((ds) => getData(getters, ds));
    if (!dataSetsData.length) {
        return datasetValues;
    }
    const minLength = Math.min(...dataSetsData.map((ds) => ds.length));
    let currentValues = [];
    const leafDatasetIndex = dataSets.length - 1;
    for (let i = 0; i < minLength; i++) {
        for (let dsIndex = 0; dsIndex < dataSetsData.length; dsIndex++) {
            let value = dataSetsData[dsIndex][i];
            if ((value === undefined || value === null) && dsIndex !== leafDatasetIndex) {
                value = currentValues[dsIndex];
            }
            if (value !== currentValues[dsIndex]) {
                currentValues = currentValues.slice(0, dsIndex);
                currentValues[dsIndex] = value;
            }
            datasetValues[dsIndex].data.push(value ?? null);
        }
    }
    return datasetValues.filter((ds) => ds.data.some((d) => d !== null));
}
function makeDatasetsCumulative(datasets, order) {
    return datasets.map((dataset) => {
        const data = [];
        let accumulator = 0;
        const indexes = order === "asc" ? Object.keys(dataset.data) : Object.keys(dataset.data).reverse();
        for (const i of indexes) {
            if (!isNaN(parseFloat(dataset.data[i]))) {
                accumulator += parseFloat(dataset.data[i]);
                data[i] = accumulator;
            }
            else {
                data[i] = dataset.data[i];
            }
        }
        return { ...dataset, data };
    });
}
function getTopPaddingForDashboard(definition, getters) {
    const { title, legendPosition } = definition;
    const hasTitleOrLegendTop = (title && title.text) || legendPosition === "top";
    return getters.isDashboard() && !hasTitleOrLegendTop ? 30 : 0;
}

function getChartLayout(definition, args) {
    return {
        padding: {
            left: CHART_PADDING,
            right: CHART_PADDING,
            top: Math.max(CHART_PADDING_TOP, args.topPadding || 0),
            bottom: CHART_PADDING_BOTTOM,
        },
    };
}

function getLegendDisplayOptions(definition, args) {
    return {
        display: definition.legendPosition !== "none",
        position: definition.legendPosition !== "none" ? definition.legendPosition : undefined,
    };
}
function getBarChartLegend(definition, args) {
    return {
        ...INTERACTIVE_LEGEND_CONFIG,
        ...getLegendDisplayOptions(definition),
        ...getCustomLegendLabels(chartFontColor(definition.background), {
            pointStyle: "rect",
            lineWidth: 3,
        }),
    };
}
function getLineChartLegend(definition, args) {
    const filled = definition.fillArea;
    const pointStyle = filled ? "rect" : "line";
    const lineWidth = filled ? 2 : 3;
    return {
        ...INTERACTIVE_LEGEND_CONFIG,
        ...getLegendDisplayOptions(definition),
        ...getCustomLegendLabels(chartFontColor(definition.background), {
            pointStyle,
            lineWidth,
        }),
    };
}
function getPieChartLegend(definition, args) {
    const { dataSetsValues } = args;
    const dataSetsLength = Math.max(0, ...dataSetsValues.map((ds) => ds?.data?.length ?? 0));
    const colors = getPieColors(new ColorGenerator(dataSetsLength), dataSetsValues);
    const fontColor = chartFontColor(definition.background);
    return {
        ...getLegendDisplayOptions(definition),
        labels: {
            usePointStyle: true,
            generateLabels: (c) => (c.data.labels?.map((label, index) => ({
                text: truncateLabel(String(label)),
                strokeStyle: colors[index],
                fillStyle: colors[index],
                pointStyle: "rect",
                lineWidth: 2,
                fontColor,
            })) || []).filter((label) => label.text),
            filter: (legendItem, data) => {
                return "datasetIndex" in legendItem
                    ? !data.datasets[legendItem.datasetIndex].hidden
                    : true;
            },
        },
    };
}
function getScatterChartLegend(definition, args) {
    return {
        ...INTERACTIVE_LEGEND_CONFIG,
        ...getLegendDisplayOptions(definition),
        ...getCustomLegendLabels(chartFontColor(definition.background), {
            pointStyle: "circle",
            // the stroke is the border around the circle, so increasing its size with the chart's color reduce the size of the circle
            strokeStyle: definition.background || "#ffffff",
            lineWidth: 8,
        }),
    };
}
function getComboChartLegend(definition, args) {
    return {
        ...INTERACTIVE_LEGEND_CONFIG,
        ...getLegendDisplayOptions(definition),
        ...getCustomLegendLabels(chartFontColor(definition.background), {
            lineWidth: 3,
        }),
    };
}
function getWaterfallChartLegend(definition, args) {
    const fontColor = chartFontColor(definition.background);
    const negativeColor = definition.negativeValuesColor || CHART_WATERFALL_NEGATIVE_COLOR;
    const positiveColor = definition.positiveValuesColor || CHART_WATERFALL_POSITIVE_COLOR;
    const subTotalColor = definition.subTotalValuesColor || CHART_WATERFALL_SUBTOTAL_COLOR;
    return {
        ...getLegendDisplayOptions(definition),
        labels: {
            usePointStyle: true,
            generateLabels: () => {
                const legendValues = [
                    {
                        text: _t("Positive values"),
                        fontColor,
                        fillStyle: positiveColor,
                        strokeStyle: positiveColor,
                        pointStyle: "rect",
                    },
                    {
                        text: _t("Negative values"),
                        fontColor,
                        fillStyle: negativeColor,
                        strokeStyle: negativeColor,
                        pointStyle: "rect",
                    },
                ];
                if (definition.showSubTotals || definition.firstValueAsSubtotal) {
                    legendValues.push({
                        text: _t("Subtotals"),
                        fontColor,
                        fillStyle: subTotalColor,
                        strokeStyle: subTotalColor,
                        pointStyle: "rect",
                    });
                }
                return legendValues;
            },
            filter: (legendItem, data) => {
                return "datasetIndex" in legendItem
                    ? !data.datasets[legendItem.datasetIndex].hidden
                    : true;
            },
        },
        onClick: () => { }, // Disables click interaction with the waterfall chart legend items
    };
}
function getRadarChartLegend(definition, args) {
    const fill = definition.fillArea ?? false;
    const pointStyle = fill ? "rect" : "line";
    const lineWidth = fill ? 2 : 3;
    return {
        ...INTERACTIVE_LEGEND_CONFIG,
        ...getLegendDisplayOptions(definition),
        ...getCustomLegendLabels(chartFontColor(definition.background), {
            pointStyle,
            lineWidth,
        }),
    };
}
function getSunburstChartLegend(definition, args) {
    const fontColor = chartFontColor(definition.background);
    return {
        ...getLegendDisplayOptions(definition),
        labels: {
            usePointStyle: true,
            generateLabels: (chart) => {
                const rootDataset = chart.data.datasets.at(-1);
                if (!rootDataset) {
                    return [];
                }
                const colors = rootDataset.groupColors;
                return colors.map(({ color, label }) => {
                    return {
                        text: truncateLabel(label),
                        fontColor,
                        fillStyle: color,
                        strokeStyle: color,
                        pointStyle: "rect",
                    };
                });
            },
        },
    };
}
/* Callback used to make the legend interactive
 * These are used to make the user able to hide/show a data series by
 * clicking on the corresponding label in the legend. The onHover and
 * onLeave callbacks are used to show a pointer when hovering an item
 * of the legend so that the user knows it is clickable.
 */
const INTERACTIVE_LEGEND_CONFIG = {
    onHover: (event) => {
        const target = event.native?.target;
        if (!target) {
            return;
        }
        //@ts-ignore
        target.style.cursor = "pointer";
    },
    onLeave: (event) => {
        const target = event.native?.target;
        if (!target) {
            return;
        }
        //@ts-ignore
        target.style.cursor = "default";
    },
    onClick: (event, legendItem, legend) => {
        if (event.type !== "click") {
            return;
        }
        const index = legendItem.datasetIndex;
        if (!legend.legendItems || index === undefined) {
            return;
        }
        if (legend.chart.isDatasetVisible(index)) {
            legend.chart.hide(index);
        }
        else {
            legend.chart.show(index);
        }
        event.native.preventDefault();
        event.native.stopPropagation();
    },
};
function getCustomLegendLabels(fontColor, legendLabelConfig) {
    return {
        labels: {
            color: fontColor,
            usePointStyle: true,
            generateLabels: (chart) => chart.data.datasets
                .map((dataset, index) => {
                if (isTrendLineAxis(dataset["xAxisID"])) {
                    return {
                        text: truncateLabel(dataset.label),
                        fontColor,
                        strokeStyle: dataset.borderColor,
                        hidden: !chart.isDatasetVisible(index),
                        pointStyle: "line",
                        datasetIndex: index,
                        lineWidth: 3,
                    };
                }
                return {
                    text: truncateLabel(dataset.label),
                    fontColor,
                    strokeStyle: dataset.borderColor,
                    fillStyle: dataset.backgroundColor,
                    hidden: !chart.isDatasetVisible(index),
                    pointStyle: dataset.type === "line" ? "line" : "rect",
                    datasetIndex: index,
                    ...legendLabelConfig,
                };
            })
                .filter((label) => label.text),
            filter: (legendItem, data) => {
                return "datasetIndex" in legendItem
                    ? !data.datasets[legendItem.datasetIndex].hidden
                    : true;
            },
        },
    };
}

function getBarChartScales(definition, args) {
    let scales = {};
    const { trendDataSetsValues: trendDatasets, locale, axisFormats } = args;
    const options = { stacked: definition.stacked, locale: locale };
    if (definition.horizontal) {
        scales.x = getChartAxis(definition, "bottom", "values", { ...options, format: axisFormats?.x });
        scales.y = getChartAxis(definition, "left", "labels", options);
    }
    else {
        scales.x = getChartAxis(definition, "bottom", "labels", options);
        const leftAxisOptions = { ...options, format: axisFormats?.y };
        scales.y = getChartAxis(definition, "left", "values", leftAxisOptions);
        const rightAxisOptions = { ...options, format: axisFormats?.y1 };
        scales.y1 = getChartAxis(definition, "right", "values", rightAxisOptions);
    }
    scales = removeFalsyAttributes(scales);
    if (trendDatasets && trendDatasets.length && trendDatasets.some(isDefined)) {
        /* We add a second x axis here to draw the trend lines, with the labels length being
         * set so that the second axis points match the classical x axis
         */
        const maxLength = Math.max(...trendDatasets.map((trendDataset) => trendDataset?.length || 0));
        scales[TREND_LINE_XAXIS_ID] = {
            ...scales.x,
            labels: Array(maxLength).fill(""),
            offset: false,
            display: false,
        };
        scales[MOVING_AVERAGE_TREND_LINE_XAXIS_ID] = {
            ...scales.x,
            offset: true,
            display: false,
        };
    }
    return scales;
}
function getLineChartScales(definition, args) {
    const { locale, axisType, trendDataSetsValues: trendDatasets, labels, axisFormats } = args;
    const labelFormat = axisFormats?.x;
    const stacked = definition.stacked;
    let scales = {
        x: getChartAxis(definition, "bottom", "labels", { locale }),
        y: getChartAxis(definition, "left", "values", { locale, stacked, format: axisFormats?.y }),
        y1: getChartAxis(definition, "right", "values", { locale, stacked, format: axisFormats?.y1 }),
    };
    scales = removeFalsyAttributes(scales);
    if (axisType === "time" && labels && labelFormat) {
        const axis = {
            type: "time",
            time: getChartTimeOptions(labels, labelFormat, locale),
        };
        Object.assign(scales.x, axis);
        scales.x.ticks.maxTicksLimit = 15;
        delete scales?.x?.ticks?.callback;
    }
    else if (axisType === "linear") {
        scales.x.type = "linear";
        scales.x.ticks.callback = definition.humanize
            ? (value) => humanizeNumber({ value, format: labelFormat }, locale)
            : (value) => formatValue(value, { format: labelFormat, locale });
    }
    if (trendDatasets && trendDatasets.length && trendDatasets.some(isDefined)) {
        /* We add a second x axis here to draw the trend lines, with the labels length being
         * set so that the second axis points match the classical x axis
         */
        scales[TREND_LINE_XAXIS_ID] = {
            ...scales.x,
            display: false,
        };
        scales[MOVING_AVERAGE_TREND_LINE_XAXIS_ID] = {
            ...scales.x,
            display: false,
        };
        if (axisType === "category" || axisType === "time") {
            /* We add a second x axis here to draw the trend lines, with the labels length being
             * set so that the second axis points match the classical x axis
             */
            const maxLength = Math.max(...trendDatasets.map((trendDataset) => trendDataset?.length || 0));
            scales[TREND_LINE_XAXIS_ID]["type"] = "category";
            scales[TREND_LINE_XAXIS_ID]["labels"] = range(0, maxLength).map((x) => x.toString());
            scales[TREND_LINE_XAXIS_ID]["offset"] = false;
            scales[MOVING_AVERAGE_TREND_LINE_XAXIS_ID]["type"] = "category";
            scales[MOVING_AVERAGE_TREND_LINE_XAXIS_ID]["offset"] = false;
        }
    }
    return scales;
}
function getScatterChartScales(definition, args) {
    const lineScales = getLineChartScales(definition, args);
    return {
        ...lineScales,
        x: {
            ...lineScales.x,
            grid: { display: true },
        },
    };
}
function getWaterfallChartScales(definition, args) {
    const { locale, axisFormats } = args;
    const format = axisFormats?.y || axisFormats?.y1;
    definition.dataSets;
    const scales = {
        x: {
            ...getChartAxis(definition, "bottom", "labels", { locale }),
            grid: { display: false },
        },
        y: {
            // TODO FIXME: we should probably remove definition.verticalAxisPosition and put everything inside axesDesign/datasets
            // like the other charts. We cannot use helpers like `getChartAxis` here because they look into definition.dataSet
            // which have plain wrong information, eg. the yAxisId of the dataset being "y" when the data is actually displayed
            // on the axis to the right.
            position: definition.verticalAxisPosition,
            ticks: {
                color: chartFontColor(definition.background),
                callback: formatTickValue({ locale, format }, definition.humanize),
            },
            grid: {
                lineWidth: (context) => (context.tick.value === 0 ? 2 : 1),
            },
            title: getChartAxisTitleRuntime(definition.axesDesign?.y),
        },
    };
    const verticalScale = scales?.y || scales?.y1;
    if (verticalScale) {
        verticalScale.grid = { lineWidth: (context) => (context.tick.value === 0 ? 2 : 1) };
    }
    return scales;
}
function getPyramidChartScales(definition, args) {
    const { dataSetsValues } = args;
    const scales = getBarChartScales(definition, args);
    const scalesXCallback = scales.x.ticks.callback;
    scales.x.ticks.callback = (value) => scalesXCallback(Math.abs(value));
    const maxValue = Math.max(...dataSetsValues.map((dataSet) => Math.max(...dataSet.data.map(Math.abs))));
    scales.x.suggestedMin = -maxValue;
    scales.x.suggestedMax = maxValue;
    return scales;
}
function getRadarChartScales(definition, args) {
    const { locale, axisFormats, dataSetsValues } = args;
    const minValue = Math.min(...dataSetsValues.map((ds) => Math.min(...ds.data.filter((x) => !isNaN(x)))));
    return {
        r: {
            beginAtZero: true,
            ticks: {
                callback: formatTickValue({ format: axisFormats?.r, locale }, definition.humanize),
                backdropColor: definition.background || "#FFFFFF",
            },
            pointLabels: {
                color: chartFontColor(definition.background),
                callback: (label) => truncateLabel(label),
            },
            suggestedMin: minValue < 0 ? minValue - 1 : 0,
        },
    };
}
function getGeoChartScales(definition, args) {
    const { locale, axisFormats, availableRegions } = args;
    const geoLegendPosition = legendPositionToGeoLegendPosition(definition.legendPosition);
    const region = definition.region
        ? availableRegions.find((r) => r.id === definition.region)
        : availableRegions[0];
    const format = axisFormats?.y || axisFormats?.y1;
    return {
        projection: {
            // projection: region?.defaultProjection,
            projection: getGeoChartProjection(region?.defaultProjection || "mercator"),
            axis: "x",
        },
        color: {
            axis: "x",
            display: definition.legendPosition !== "none",
            border: { color: GRAY_300 },
            grid: { color: GRAY_300 },
            ticks: {
                color: chartFontColor(definition.background),
                callback: formatTickValue({ locale, format }, definition.humanize),
            },
            legend: {
                position: geoLegendPosition,
                align: geoLegendPosition.includes("right") ? "left" : "right",
                margin: getLegendMargin(definition),
            },
            interpolate: getRuntimeColorScale(definition),
            missing: definition.missingValueColor || "#ffffff",
        },
    };
}
function getFunnelChartScales(definition, args) {
    const dataSet = args.dataSetsValues[0];
    return {
        x: {
            display: false,
        },
        y: {
            grid: { offset: false }, // bar charts grid is offset by default
            ticks: {
                callback: function (tickValue) {
                    return truncateLabel(this.getLabelForValue(tickValue));
                },
            },
            border: { display: false },
        },
        percentages: {
            position: "right",
            border: { display: false },
            ticks: {
                callback: function (tickValue, index, ticks) {
                    const value = dataSet.data?.[index];
                    const baseValue = dataSet.data?.[0];
                    if (!baseValue || value === undefined) {
                        return "";
                    }
                    return formatValue(value / baseValue, { format: "0%", locale: args.locale });
                },
            },
            grid: { display: false },
        },
    };
}
function getGeoChartProjection(projection) {
    if (projection === "conicConformal") {
        return window.ChartGeo.geoConicConformal().rotate([100, 0]); // Centered on the US
    }
    return projection;
}
function getChartAxisTitleRuntime(design) {
    if (design?.title?.text) {
        const { text, color, align, italic, bold } = design.title;
        return {
            display: true,
            text,
            color,
            font: {
                style: italic ? "italic" : "normal",
                weight: bold ? "bold" : "normal",
                size: design.title.fontSize ?? CHART_AXIS_TITLE_FONT_SIZE,
            },
            align: align === "left" ? "start" : align === "right" ? "end" : "center",
        };
    }
    return;
}
function getChartAxis(definition, position, type, options) {
    const { useLeftAxis, useRightAxis } = getDefinedAxis(definition);
    if ((position === "left" && !useLeftAxis) || (position === "right" && !useRightAxis)) {
        return undefined;
    }
    const fontColor = chartFontColor(definition.background);
    let design;
    if (position === "bottom") {
        design = definition.axesDesign?.x;
    }
    else if (position === "left") {
        design = definition.axesDesign?.y;
    }
    else {
        design = definition.axesDesign?.y1;
    }
    if (type === "values") {
        const displayGridLines = !(position === "right" && useLeftAxis);
        return {
            position: position,
            title: getChartAxisTitleRuntime(design),
            grid: {
                display: displayGridLines,
            },
            beginAtZero: true,
            stacked: options?.stacked,
            ticks: {
                color: fontColor,
                callback: formatTickValue(options, definition.humanize),
            },
        };
    }
    else {
        return {
            ticks: {
                padding: 5,
                color: fontColor,
                callback: function (tickValue, index, ticks) {
                    // Category axis callback's internal tick value is the index of the label
                    // https://www.chartjs.org/docs/latest/axes/labelling.html#creating-custom-tick-formats
                    return truncateLabel(this.getLabelForValue(tickValue));
                },
            },
            grid: {
                display: false,
            },
            stacked: options?.stacked,
            title: getChartAxisTitleRuntime(design),
        };
    }
}
function getRuntimeColorScale(definition) {
    if (!definition.colorScale || typeof definition.colorScale === "string") {
        return definition.colorScale || "oranges";
    }
    const scaleColors = [{ value: 0, color: definition.colorScale.minColor }];
    if (definition.colorScale.midColor) {
        scaleColors.push({ value: 0.5, color: definition.colorScale.midColor });
    }
    scaleColors.push({ value: 1, color: definition.colorScale.maxColor });
    return getColorScale(scaleColors);
}
function getLegendMargin(definition) {
    switch (definition.legendPosition) {
        case "top":
        case "right":
            const hasTitle = !!definition.title.text;
            const topMargin = hasTitle ? CHART_PADDING_TOP + 30 : CHART_PADDING_TOP;
            return { top: topMargin, left: CHART_PADDING, right: CHART_PADDING };
        case "bottom":
        case "left":
        case "none":
            return { left: CHART_PADDING, right: CHART_PADDING, bottom: CHART_PADDING_BOTTOM };
    }
}
function legendPositionToGeoLegendPosition(position) {
    switch (position) {
        case "top":
            return "top-left";
        case "right":
            return "top-right";
        case "bottom":
            return "bottom-right";
        case "left":
            return "bottom-left";
        case "none":
            return "bottom-left";
    }
}

const SunburstChartDefaults = {
    showValues: false,
    showLabels: true,
    valuesDesign: {
        align: "center",
        fontSize: 13,
    },
};

function getChartShowValues(definition, args) {
    const { axisFormats, locale } = args;
    return {
        horizontal: "horizontal" in definition && definition.horizontal,
        showValues: "showValues" in definition ? !!definition.showValues : false,
        background: definition.background,
        callback: (value, dataset) => {
            const axisId = getDatasetAxisId(definition, dataset);
            return formatChartDatasetValue(axisFormats, locale, definition.humanize)(value, axisId);
        },
    };
}
function getSunburstShowValues(definition, args) {
    const { axisFormats, locale } = args;
    return {
        callback: formatChartDatasetValue(axisFormats, locale, definition.humanize),
        showLabels: definition.showLabels ?? SunburstChartDefaults.showLabels,
        showValues: definition.showValues ?? SunburstChartDefaults.showValues,
        style: {
            fontSize: definition.valuesDesign?.fontSize ?? SunburstChartDefaults.valuesDesign.fontSize,
            align: definition.valuesDesign?.align ?? SunburstChartDefaults.valuesDesign.align,
            bold: definition.valuesDesign?.bold ?? SunburstChartDefaults.valuesDesign.bold,
            italic: definition.valuesDesign?.italic ?? SunburstChartDefaults.valuesDesign.italic,
            textColor: definition.valuesDesign?.color ?? SunburstChartDefaults.valuesDesign.color,
        },
    };
}
function getPyramidChartShowValues(definition, args) {
    const { axisFormats, locale } = args;
    return {
        horizontal: true,
        showValues: "showValues" in definition ? !!definition.showValues : false,
        background: definition.background,
        callback: (value, dataset) => {
            value = Math.abs(Number(value));
            return value === 0
                ? ""
                : formatChartDatasetValue(axisFormats, locale)(value, dataset.xAxisID || "x");
        },
    };
}
function getWaterfallChartShowValues(definition, args) {
    const { axisFormats, locale, dataSetsValues } = args;
    const subtotalIndexes = dataSetsValues.reduce((subtotalIndexes, ds) => {
        subtotalIndexes.push((subtotalIndexes.at(-1) || -1) + ds.data.length + 1);
        return subtotalIndexes;
    }, []);
    return {
        showValues: "showValues" in definition ? !!definition.showValues : false,
        background: definition.background,
        callback: (value, dataset, index) => {
            const raw = dataset._dataset.data[index];
            const delta = raw[1] - raw[0];
            let sign = delta >= 0 ? "+" : "";
            if (definition.showSubTotals && subtotalIndexes.includes(index) && sign === "+") {
                sign = "";
            }
            return `${sign}${formatChartDatasetValue(axisFormats, locale, definition.humanize)(delta, dataset.yAxisID)}`;
        },
    };
}
function getDatasetAxisId(definition, dataset) {
    if (dataset.rAxisID) {
        return dataset.rAxisID;
    }
    const axisId = "horizontal" in definition && definition.horizontal ? dataset.xAxisID : dataset.yAxisID;
    return axisId || "y";
}

function getChartTitle(definition, getters) {
    const chartTitle = definition.title;
    const fontColor = chartMutedFontColor(definition.background);
    return {
        display: !!chartTitle.text,
        text: chartTitle.text ? getters.dynamicTranslate(chartTitle.text) : "",
        color: chartTitle?.color ?? fontColor,
        align: chartTitle.align === "center" ? "center" : chartTitle.align === "right" ? "end" : "start",
        font: {
            size: definition.title.fontSize ?? CHART_TITLE_FONT_SIZE,
            weight: chartTitle.bold ? "bold" : "normal",
            style: chartTitle.italic ? "italic" : "normal",
        },
        padding: {
            // Disable title top/left/right padding to use the chart padding instead.
            // The legend already has a top padding, so bottom padding is useless for the title there.
            bottom: definition.legendPosition === "top" ? 0 : CHART_PADDING,
        },
    };
}

/**
 * Custom tooltip for the charts. Mostly copied from Odoo's custom tooltip, with some slight changes to make it work
 * with o-spreadsheet chart data and CSS.
 *
 * https://github.com/odoo/odoo/blob/18.0/addons/web/static/src/views/graph/graph_renderer.xml
 */
const templates = /* xml */ `
<templates>
  <t t-name="o-spreadsheet-CustomTooltip">
    <div
      class="o-chart-custom-tooltip border rounded px-2 py-1 pe-none mw-100 position-absolute text-nowrap shadow opacity-100">
      <table class="overflow-hidden m-0">
        <thead t-if="title">
          <tr>
            <th class="o-tooltip-title align-baseline border-0 text-truncate" t-esc="title" t-attf-style="max-width: {{ labelsMaxWidth }}"/>
          </tr>
        </thead>
        <tbody>
          <tr t-foreach="tooltipItems" t-as="tooltipItem" t-key="tooltipItem_index">
            <td>
              <span
                class="badge ps-2 py-2 rounded-0 align-middle"
                t-attf-style="background-color: {{ tooltipItem.boxColor }}"
              > </span>
              <small
                t-if="tooltipItem.label"
                class="o-tooltip-label d-inline-block text-truncate align-middle smaller ms-2"
                t-esc="tooltipItem.label"
                t-attf-style="max-width: {{ labelsMaxWidth }}"
              />
            </td>
            <td class="o-tooltip-value ps-2 fw-bolder text-end">
              <small class="smaller d-inline-block text-truncate align-middle" t-attf-style="max-width: {{ valuesMaxWidth }}">
                <t t-esc="tooltipItem.value"/>
                <t t-if="tooltipItem.percentage">
                  (
                  <t t-esc="tooltipItem.percentage"/>
                  %)
                </t>
              </small>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  </t>
</templates>
`;
let app;
function renderToString(templateName, context = {}) {
    return render(templateName, context).innerHTML;
}
function render(templateName, context = {}) {
    if (!app) {
        app = new App(Component, { templates, translateFn: _t });
    }
    const templateFn = app.getTemplate(templateName);
    const bdom = templateFn(context, {});
    const div = document.createElement("div");
    blockDom.mount(bdom, div);
    return div;
}

function getBarChartTooltip(definition, args) {
    return {
        enabled: false,
        external: customTooltipHandler,
        callbacks: {
            title: function (tooltipItems) {
                return tooltipItems.some((item) => !isTrendLineAxis(item.dataset.xAxisID)) ? undefined : "";
            },
            beforeLabel: (tooltipItem) => tooltipItem.dataset?.label || tooltipItem.label,
            label: function (tooltipItem) {
                const horizontalChart = definition.horizontal;
                let yLabel = horizontalChart ? tooltipItem.parsed.x : tooltipItem.parsed.y;
                if (yLabel === undefined || yLabel === null) {
                    yLabel = tooltipItem.parsed;
                }
                const axisId = horizontalChart ? tooltipItem.dataset.xAxisID : tooltipItem.dataset.yAxisID;
                const yLabelStr = formatChartDatasetValue(args.axisFormats, args.locale, definition.humanize)(yLabel, axisId);
                return yLabelStr;
            },
        },
    };
}
function getLineChartTooltip(definition, args) {
    const { axisType, locale, axisFormats } = args;
    const labelFormat = axisFormats?.x;
    const tooltip = {
        enabled: false,
        external: customTooltipHandler,
        callbacks: {},
    };
    if (axisType === "linear") {
        tooltip.callbacks.label = (tooltipItem) => {
            const dataSetPoint = tooltipItem.parsed.y;
            let label = isTrendLineAxis(tooltipItem.dataset.xAxisID)
                ? ""
                : tooltipItem.parsed.x;
            if (typeof label === "string" && isNumber(label, locale)) {
                label = toNumber(label, locale);
            }
            const formattedX = formatOrHumanizeValue(label, labelFormat, locale, definition.humanize);
            const axisId = tooltipItem.dataset.yAxisID || "y";
            const formattedY = formatOrHumanizeValue(dataSetPoint, axisFormats?.[axisId], locale, definition.humanize);
            return formattedX ? `(${formattedX}, ${formattedY})` : `${formattedY}`;
        };
    }
    else {
        tooltip.callbacks.label = function (tooltipItem) {
            const yLabel = tooltipItem.parsed.y;
            const axisId = tooltipItem.dataset.yAxisID;
            const yLabelStr = formatChartDatasetValue(axisFormats, locale, definition.humanize)(yLabel, axisId);
            return yLabelStr;
        };
    }
    tooltip.callbacks.beforeLabel = (tooltipItem) => tooltipItem.dataset?.label || tooltipItem.label;
    tooltip.callbacks.title = function (tooltipItems) {
        const displayTooltipTitle = axisType !== "linear" && tooltipItems.some((item) => !isTrendLineAxis(item.dataset.xAxisID));
        return displayTooltipTitle ? undefined : "";
    };
    return tooltip;
}
function getPieChartTooltip(definition, args) {
    const { locale, axisFormats } = args;
    const format = axisFormats?.y || axisFormats?.y1;
    return {
        enabled: false,
        external: customTooltipHandler,
        callbacks: {
            title: function (tooltipItems) {
                return tooltipItems[0].dataset.label;
            },
            beforeLabel: (tooltipItem) => tooltipItem.label || tooltipItem.dataset.label,
            label: function (tooltipItem) {
                const data = tooltipItem.dataset.data;
                const dataIndex = tooltipItem.dataIndex;
                const percentage = calculatePercentage(data, dataIndex);
                const yLabel = tooltipItem.parsed.y ?? tooltipItem.parsed;
                const toolTipFormat = !format && yLabel >= 1000 ? "#,##" : format;
                const yLabelStr = formatOrHumanizeValue(yLabel, toolTipFormat, locale, definition.humanize);
                return `${yLabelStr} (${percentage}%)`;
            },
        },
    };
}
function getWaterfallChartTooltip(definition, args) {
    const { dataSetsValues, locale, axisFormats, labels } = args;
    const format = axisFormats?.y || axisFormats?.y1;
    const dataSeriesLabels = dataSetsValues.map((dataSet) => dataSet.label);
    return {
        enabled: false,
        external: customTooltipHandler,
        callbacks: {
            beforeLabel: function (tooltipItem) {
                const dataSeriesIndex = labels.length
                    ? Math.floor(tooltipItem.dataIndex / labels.length)
                    : 0;
                return dataSeriesLabels[dataSeriesIndex];
            },
            label: function (tooltipItem) {
                const [lastValue, currentValue] = tooltipItem.raw;
                const yLabel = currentValue - lastValue;
                const toolTipFormat = !format && Math.abs(yLabel) > 1000 ? "#,##" : format;
                return formatOrHumanizeValue(yLabel, toolTipFormat, locale, definition.humanize);
            },
        },
    };
}
function getPyramidChartTooltip(definition, args) {
    const tooltip = getBarChartTooltip(definition, args);
    return {
        ...tooltip,
        callbacks: {
            ...tooltip.callbacks,
            label: (item) => {
                const tooltipItem = { ...item, parsed: { y: item.parsed.y, x: Math.abs(item.parsed.x) } };
                return (tooltip?.callbacks?.label)(tooltipItem);
            },
        },
    };
}
function getRadarChartTooltip(definition, args) {
    const { locale, axisFormats } = args;
    return {
        enabled: false,
        external: customTooltipHandler,
        callbacks: {
            beforeLabel: (tooltipItem) => tooltipItem.dataset?.label || tooltipItem.label,
            label: function (tooltipItem) {
                const yLabel = tooltipItem.parsed.r;
                return formatOrHumanizeValue(yLabel, axisFormats?.r, locale, definition.humanize);
            },
        },
    };
}
function getGeoChartTooltip(definition, args) {
    const { locale, axisFormats } = args;
    const format = axisFormats?.y || axisFormats?.y1;
    return {
        enabled: false,
        external: customTooltipHandler,
        filter: function (tooltipItem) {
            return tooltipItem.raw.value !== undefined;
        },
        callbacks: {
            beforeLabel: (tooltipItem) => tooltipItem.raw.feature.properties.name,
            label: function (tooltipItem) {
                const rawItem = tooltipItem.raw;
                const yLabel = rawItem.value;
                const toolTipFormat = !format && Math.abs(yLabel) >= 1000 ? "#,##" : format;
                return formatOrHumanizeValue(yLabel, toolTipFormat, locale, definition.humanize);
            },
        },
    };
}
function getFunnelChartTooltip(definition, args) {
    return {
        enabled: false,
        external: customTooltipHandler,
        position: "funnelTooltipPositioner",
        callbacks: {
            title: () => "",
            beforeLabel: (tooltipItem) => tooltipItem.label,
            label: function (tooltipItem) {
                const yLabel = tooltipItem.parsed.x;
                const axisId = tooltipItem.dataset.xAxisID;
                const yLabelStr = formatChartDatasetValue(args.axisFormats, args.locale, definition.humanize)(yLabel, axisId);
                return yLabelStr;
            },
        },
    };
}
function getSunburstChartTooltip(definition, args) {
    const { locale, axisFormats } = args;
    const format = axisFormats?.y || axisFormats?.y1;
    return {
        enabled: false,
        external: customTooltipHandler,
        filter: function (tooltipItem) {
            const data = tooltipItem.raw;
            return data?.label !== GHOST_SUNBURST_VALUE;
        },
        callbacks: {
            title: () => "",
            beforeLabel: (tooltipItem) => {
                const data = tooltipItem.raw;
                return data.groups.join(" / ");
            },
            label: function (tooltipItem) {
                const data = tooltipItem.raw;
                const yLabel = data.value;
                const toolTipFormat = !format && yLabel >= 1000 ? "#,##" : format;
                return formatOrHumanizeValue(yLabel, toolTipFormat, locale, definition.humanize);
            },
        },
    };
}
function getTreeMapChartTooltip(definition, args) {
    const { locale, axisFormats } = args;
    const format = axisFormats?.y;
    return {
        enabled: false,
        external: customTooltipHandler,
        filter: (tooltipItem, index, tooltipItems) => {
            return index === tooltipItems.length - 1;
        },
        callbacks: {
            title: () => "",
            beforeLabel: (tooltipItem) => {
                const childElement = tooltipItem.raw._data.children[0];
                if (!childElement) {
                    return "";
                }
                const path = [];
                for (let i = 0; i <= tooltipItem.raw.l; i++) {
                    path.push(childElement[i]);
                }
                return path.join(" / ");
            },
            label: (tooltipItem) => {
                const yLabel = tooltipItem.raw.v;
                const toolTipFormat = !format && yLabel >= 1000 ? "#,##" : format;
                return formatOrHumanizeValue(yLabel, toolTipFormat, locale, definition.humanize);
            },
        },
    };
}
function calculatePercentage(dataset, dataIndex) {
    const numericData = dataset.filter((value) => typeof value === "number");
    const total = numericData.reduce((sum, value) => sum + value, 0);
    if (!total) {
        return "";
    }
    const percentage = (dataset[dataIndex] / total) * 100;
    return percentage.toFixed(2);
}
function customTooltipHandler({ chart, tooltip }) {
    chart.canvas.parentNode.querySelector("div.o-chart-custom-tooltip")?.remove();
    if (tooltip.opacity === 0 || tooltip.dataPoints.length === 0) {
        return;
    }
    const tooltipItems = tooltip.body.map((body, index) => {
        let label = body.before[0];
        let value = body.lines[0];
        if (!value) {
            value = label;
            label = "";
        }
        const color = tooltip.labelColors[index].backgroundColor;
        return {
            label,
            value,
            boxColor: typeof color === "string" ? setColorAlpha(color, 1) : color,
        };
    });
    const innerHTML = renderToString("o-spreadsheet-CustomTooltip", {
        labelsMaxWidth: Math.floor(chart.canvas.clientWidth * 0.5) + "px",
        valuesMaxWidth: Math.floor(chart.canvas.clientWidth * 0.25) + "px",
        title: tooltip.title[0],
        tooltipItems,
    });
    const template = Object.assign(document.createElement("template"), { innerHTML });
    const newTooltipEl = template.content.firstChild;
    chart.canvas.parentNode?.appendChild(newTooltipEl);
    Object.assign(newTooltipEl.style, {
        left: getTooltipLeftPosition(chart, tooltip, newTooltipEl.clientWidth) + "px",
        top: Math.floor(tooltip.caretY - newTooltipEl.clientHeight / 2) + "px",
    });
}
/**
 * Get the left position for the tooltip, making sure it doesn't go out of the chart area.
 */
function getTooltipLeftPosition(chart, tooltip, tooltipWidth) {
    const x = tooltip.caretX;
    if (x + tooltipWidth > chart.chartArea.right) {
        return Math.max(0, x - tooltipWidth);
    }
    return x;
}

var CHART_RUNTIME_HELPERS = /*#__PURE__*/Object.freeze({
    __proto__: null,
    GHOST_SUNBURST_VALUE: GHOST_SUNBURST_VALUE,
    INTERACTIVE_LEGEND_CONFIG: INTERACTIVE_LEGEND_CONFIG,
    canChartParseLabels: canChartParseLabels,
    getBarChartData: getBarChartData,
    getBarChartDatasets: getBarChartDatasets,
    getBarChartLegend: getBarChartLegend,
    getBarChartScales: getBarChartScales,
    getBarChartTooltip: getBarChartTooltip,
    getChartColorsGenerator: getChartColorsGenerator,
    getChartLabelFormat: getChartLabelFormat,
    getChartLayout: getChartLayout,
    getChartShowValues: getChartShowValues,
    getChartTitle: getChartTitle,
    getComboChartDatasets: getComboChartDatasets,
    getComboChartLegend: getComboChartLegend,
    getData: getData,
    getFunnelChartData: getFunnelChartData,
    getFunnelChartDatasets: getFunnelChartDatasets,
    getFunnelChartScales: getFunnelChartScales,
    getFunnelChartTooltip: getFunnelChartTooltip,
    getFunnelLabelColors: getFunnelLabelColors,
    getGeoChartData: getGeoChartData,
    getGeoChartDatasets: getGeoChartDatasets,
    getGeoChartScales: getGeoChartScales,
    getGeoChartTooltip: getGeoChartTooltip,
    getHierarchalChartData: getHierarchalChartData,
    getLineChartData: getLineChartData,
    getLineChartDatasets: getLineChartDatasets,
    getLineChartLegend: getLineChartLegend,
    getLineChartScales: getLineChartScales,
    getLineChartTooltip: getLineChartTooltip,
    getPieChartData: getPieChartData,
    getPieChartDatasets: getPieChartDatasets,
    getPieChartLegend: getPieChartLegend,
    getPieChartTooltip: getPieChartTooltip,
    getPyramidChartData: getPyramidChartData,
    getPyramidChartScales: getPyramidChartScales,
    getPyramidChartShowValues: getPyramidChartShowValues,
    getPyramidChartTooltip: getPyramidChartTooltip,
    getRadarChartData: getRadarChartData,
    getRadarChartDatasets: getRadarChartDatasets,
    getRadarChartLegend: getRadarChartLegend,
    getRadarChartScales: getRadarChartScales,
    getRadarChartTooltip: getRadarChartTooltip,
    getScatterChartDatasets: getScatterChartDatasets,
    getScatterChartLegend: getScatterChartLegend,
    getScatterChartScales: getScatterChartScales,
    getSunburstChartDatasets: getSunburstChartDatasets,
    getSunburstChartLegend: getSunburstChartLegend,
    getSunburstChartTooltip: getSunburstChartTooltip,
    getSunburstShowValues: getSunburstShowValues,
    getTopPaddingForDashboard: getTopPaddingForDashboard,
    getTreeMapChartDatasets: getTreeMapChartDatasets,
    getTreeMapChartTooltip: getTreeMapChartTooltip,
    getTrendDatasetForBarChart: getTrendDatasetForBarChart,
    getTrendDatasetForLineChart: getTrendDatasetForLineChart,
    getWaterfallChartLegend: getWaterfallChartLegend,
    getWaterfallChartScales: getWaterfallChartScales,
    getWaterfallChartShowValues: getWaterfallChartShowValues,
    getWaterfallChartTooltip: getWaterfallChartTooltip,
    getWaterfallDatasetAndLabels: getWaterfallDatasetAndLabels,
    makeDatasetsCumulative: makeDatasetsCumulative
});

class BarChart extends AbstractChart {
    dataSets;
    labelRange;
    background;
    legendPosition;
    stacked;
    aggregated;
    type = "bar";
    dataSetsHaveTitle;
    dataSetDesign;
    axesDesign;
    horizontal;
    showValues;
    zoomable;
    constructor(definition, sheetId, getters) {
        super(definition, sheetId, getters);
        this.dataSets = createDataSets(getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);
        this.labelRange = createValidRange(getters, sheetId, definition.labelRange);
        this.background = definition.background;
        this.legendPosition = definition.legendPosition;
        this.stacked = definition.stacked;
        this.aggregated = definition.aggregated;
        this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
        this.dataSetDesign = definition.dataSets;
        this.axesDesign = definition.axesDesign;
        this.horizontal = definition.horizontal;
        this.showValues = definition.showValues;
        this.zoomable = definition.zoomable;
    }
    static transformDefinition(chartSheetId, definition, applyChange) {
        return transformChartDefinitionWithDataSetsWithZone(chartSheetId, definition, applyChange);
    }
    static validateChartDefinition(validator, definition) {
        return validator.checkValidations(definition, checkDataset, checkLabelRange);
    }
    static getDefinitionFromContextCreation(context) {
        return {
            background: context.background,
            dataSets: context.range ?? [],
            dataSetsHaveTitle: context.dataSetsHaveTitle ?? false,
            stacked: context.stacked ?? false,
            aggregated: context.aggregated ?? false,
            legendPosition: context.legendPosition ?? "top",
            title: context.title || { text: "" },
            type: "bar",
            labelRange: context.auxiliaryRange || undefined,
            axesDesign: context.axesDesign,
            showValues: context.showValues,
            horizontal: context.horizontal,
            zoomable: context.zoomable,
            humanize: context.humanize,
        };
    }
    getContextCreation() {
        const range = [];
        for (const [i, dataSet] of this.dataSets.entries()) {
            range.push({
                ...this.dataSetDesign?.[i],
                dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId),
            });
        }
        return {
            ...this,
            range,
            auxiliaryRange: this.labelRange
                ? this.getters.getRangeString(this.labelRange, this.sheetId)
                : undefined,
        };
    }
    duplicateInDuplicatedSheet(newSheetId) {
        const dataSets = duplicateDataSetsInDuplicatedSheet(this.sheetId, newSheetId, this.dataSets);
        const labelRange = duplicateLabelRangeInDuplicatedSheet(this.sheetId, newSheetId, this.labelRange);
        const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, newSheetId);
        return new BarChart(definition, newSheetId, 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) {
        const ranges = [];
        for (const [i, dataSet] of dataSets.entries()) {
            ranges.push({
                ...this.dataSetDesign?.[i],
                dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId),
            });
        }
        return {
            type: "bar",
            dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,
            background: this.background,
            dataSets: ranges,
            legendPosition: this.legendPosition,
            labelRange: labelRange
                ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)
                : undefined,
            title: this.title,
            stacked: this.stacked,
            aggregated: this.aggregated,
            axesDesign: this.axesDesign,
            horizontal: this.horizontal,
            showValues: this.showValues,
            zoomable: this.horizontal ? undefined : this.zoomable,
            humanize: this.humanize,
        };
    }
    getDefinitionForExcel() {
        const { dataSets, labelRange } = this.getCommonDataSetAttributesForExcel(this.labelRange, this.dataSets, shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle));
        const definition = this.getDefinition();
        return {
            ...definition,
            backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),
            fontColor: toXlsxHexColor(chartFontColor(this.background)),
            dataSets,
            labelRange,
            verticalAxis: getDefinedAxis(definition),
        };
    }
    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 createBarChartRuntime(chart, getters) {
    const definition = chart.getDefinition();
    const chartData = getBarChartData(definition, chart.dataSets, chart.labelRange, getters);
    const config = {
        type: "bar",
        data: {
            labels: chartData.labels,
            datasets: getBarChartDatasets(definition, chartData),
        },
        options: {
            ...CHART_COMMON_OPTIONS,
            indexAxis: chart.horizontal ? "y" : "x",
            layout: getChartLayout(definition, chartData),
            scales: getBarChartScales(definition, chartData),
            plugins: {
                title: getChartTitle(definition, getters),
                legend: getBarChartLegend(definition),
                tooltip: getBarChartTooltip(definition, chartData),
                chartShowValuesPlugin: getChartShowValues(definition, chartData),
            },
        },
    };
    return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
}

const TREND_LINE_AXES_IDS = [TREND_LINE_XAXIS_ID, MOVING_AVERAGE_TREND_LINE_XAXIS_ID];
const ZOOMABLE_AXIS_IDS = ["x", ...TREND_LINE_AXES_IDS];
class ZoomableChartStore extends SpreadsheetStore {
    mutators = [
        "resetAxisLimits",
        "updateAxisLimits",
        "updateTrendLineConfiguration",
        "clearAxisLimits",
    ];
    originalAxisLimits = {};
    currentAxesLimits = {};
    idConversion = {};
    handle(cmd) {
        switch (cmd.type) {
            case "DELETE_FIGURE":
                if (cmd.figureId && this.idConversion[cmd.figureId]) {
                    for (const chartId of this.idConversion[cmd.figureId]) {
                        delete this.originalAxisLimits[chartId];
                        delete this.currentAxesLimits[chartId];
                    }
                }
                break;
            case "UPDATE_CHART":
                const type = cmd.definition.type;
                const chartId = `${type}-${cmd.figureId}`;
                if (!this.idConversion[cmd.figureId]) {
                    this.idConversion[cmd.figureId] = new Set();
                }
                this.idConversion[cmd.figureId].add(chartId);
                if (!("zoomable" in cmd.definition && cmd.definition.zoomable)) {
                    this.clearAxisLimits(chartId);
                }
                break;
        }
    }
    clearAxisLimits(chartId) {
        delete this.originalAxisLimits[chartId];
        delete this.currentAxesLimits[chartId];
        return "noStateChange";
    }
    resetAxisLimits(chartId, limits) {
        for (const axisId of ZOOMABLE_AXIS_IDS) {
            if (limits[axisId]) {
                this.originalAxisLimits[chartId] = {
                    ...this.originalAxisLimits[chartId],
                    [axisId]: { ...limits[axisId] },
                };
            }
            else if (this.originalAxisLimits[chartId]?.[axisId]) {
                delete this.originalAxisLimits[chartId][axisId];
            }
        }
        return "noStateChange";
    }
    updateAxisLimits(chartId, limits) {
        let { min, max } = limits;
        if (min > max) {
            [min, max] = [max, min];
        }
        this.currentAxesLimits[chartId] = { x: { min, max } };
        return "noStateChange";
    }
    /* Update the trend line axis configuration based on the current axis limits.
     * This function calculates the new limits for the trend line axes based on the current x-axis
     * limits and the original limits of the trend line axes.
     * It assumes that the origininal trend line axes are linear transformations of the original x-axis
     * limits and applies the same transformation to the current x-axis limits to get the new limits
     * for the current trend line axes.
     */
    updateTrendLineConfiguration(chartId) {
        if (!this.originalAxisLimits[chartId]?.x || !this.currentAxesLimits[chartId]?.x) {
            return "noStateChange";
        }
        const chartLimits = this.originalAxisLimits[chartId].x;
        for (const axisId of TREND_LINE_AXES_IDS) {
            if (!this.originalAxisLimits[chartId][axisId]) {
                continue;
            }
            const realRange = chartLimits.max - chartLimits.min;
            const trendingLimits = this.originalAxisLimits[chartId][axisId];
            const trendingRange = trendingLimits.max - trendingLimits.min;
            const slope = trendingRange / realRange;
            const intercept = trendingLimits.min - chartLimits.min * slope;
            const newXMin = this.currentAxesLimits[chartId].x.min;
            const newXMax = this.currentAxesLimits[chartId].x.max;
            this.currentAxesLimits[chartId][axisId] = {
                min: newXMin * slope + intercept,
                max: newXMax * slope + intercept,
            };
        }
        return "noStateChange";
    }
}

const zoomWindowPlugin = {
    id: "zoomWindowPlugin",
    afterDatasetsDraw: function (chart, args, options) {
        if (!options.getLowerBound || !options.getUpperBound) {
            return;
        }
        const { ctx, chartArea: { left, right, top, bottom }, } = chart;
        let lowerBound = options.getLowerBound() ?? left;
        let upperBound = options.getUpperBound() ?? right;
        if (lowerBound > upperBound) {
            [lowerBound, upperBound] = [upperBound, lowerBound];
        }
        lowerBound = Math.max(left, lowerBound);
        upperBound = Math.min(right, upperBound);
        if (lowerBound === left) {
            lowerBound -= 1;
        }
        if (upperBound === right) {
            upperBound += 1;
        }
        ctx.save();
        ctx.fillStyle = "rgba(255,255,255,0.5)";
        ctx.beginPath();
        ctx.rect(left, bottom, lowerBound - left, top - bottom);
        ctx.rect(upperBound, bottom, right - upperBound, top - bottom);
        ctx.fill();
        ctx.beginPath();
        ctx.strokeStyle = "#bbb";
        ctx.rect(lowerBound, bottom, upperBound - lowerBound, top - bottom);
        ctx.stroke();
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.moveTo(lowerBound - 3, (top + bottom) / 2 - 7);
        ctx.lineTo(lowerBound - 3, (top + bottom) / 2 + 7);
        ctx.stroke();
        ctx.beginPath();
        ctx.moveTo(upperBound + 3, (top + bottom) / 2 - 7);
        ctx.lineTo(upperBound + 3, (top + bottom) / 2 + 7);
        ctx.stroke();
        ctx.restore();
    },
};

css /* scss */ `
  .o-spreadsheet {
    .o-master-chart-container {
      height: ${MASTER_CHART_HEIGHT}px;
    }
  }
`;
chartJsExtensionRegistry.add("zoomWindowPlugin", {
    register: (Chart) => Chart.register(zoomWindowPlugin),
    unregister: (Chart) => Chart.unregister(zoomWindowPlugin),
});
class ZoomableChartJsComponent extends ChartJsComponent {
    static template = "o-spreadsheet-ZoomableChartJsComponent";
    store;
    masterChartCanvas = useRef("masterChartCanvas");
    masterChart;
    mode;
    hasLinearScale;
    isBarChart;
    chartId = "";
    datasetBoundaries = { min: 0, max: 0 };
    removeEventListeners = () => { };
    isMasterChartAllowed = false;
    setup() {
        this.store = useStore(ZoomableChartStore);
        super.setup();
    }
    unmount() {
        super.unmount();
        this.masterChart?.destroy();
        this.removeEventListeners();
    }
    get containerStyle() {
        const height = this.sliceable ? `calc(100% - ${MASTER_CHART_HEIGHT}px)` : "100%";
        return `
      height:${height};
    `;
    }
    get masterChartContainerStyle() {
        const runtime = this.env.model.getters.getChartRuntime(this.props.chartId);
        if (runtime && !runtime.chartJsConfig.data.datasets.some((ds) => ds.data.length > 1)) {
            return "opacity: 0.3;";
        }
        return "";
    }
    get sliceable() {
        if (this.props.isFullScreen) {
            return true;
        }
        const definition = this.env.model.getters.getChartDefinition(this.props.chartId);
        return ("zoomable" in definition && definition.zoomable) ?? false;
    }
    get axisOffset() {
        return !this.hasLinearScale && this.isBarChart ? 0.5 : 0;
    }
    getMasterChartConfiguration(chartData) {
        const config = chartData;
        return {
            ...config,
            options: {
                ...config.options,
                plugins: {
                    ...config.options.plugins,
                    zoomWindowPlugin: {
                        getLowerBound: () => this.lowerBound,
                        getUpperBound: () => this.upperBound,
                    },
                },
            },
        };
    }
    getDetailChartConfiguration(chartData) {
        if (!this.sliceable) {
            return chartData;
        }
        let x = chartData.options.scales.x;
        const limits = this.store.currentAxesLimits[this.chartId]?.x;
        if (limits) {
            x = {
                ...x,
                ...this.getStoredBoundaries(),
            };
        }
        return {
            ...chartData,
            options: {
                ...chartData.options,
                scales: {
                    ...chartData.options.scales,
                    x,
                },
                layout: {
                    ...chartData.options.layout,
                    padding: {
                        ...chartData.options.layout?.padding,
                        bottom: 5,
                    },
                },
            },
        };
    }
    getAxisLimitsFromDataset(chartData) {
        const data = chartData.data.datasets.map((ds) => ds.data).flat();
        const xValues = data.map((d, i) => (typeof d === "object" && d !== null ? d.x : i));
        const min = Math.min(...xValues);
        const max = Math.max(...xValues);
        return { min, max };
    }
    setMasterChartCursor(runtime) {
        const masterElement = this.masterChartCanvas?.el;
        if (runtime && !runtime.chartJsConfig.data.datasets.some((ds) => ds.data.length > 1)) {
            masterElement.style.cursor = "not-allowed";
            this.isMasterChartAllowed = false;
            return;
        }
        masterElement.style.cursor = "default";
        this.isMasterChartAllowed = true;
    }
    createChart(chartRuntime) {
        const chartData = chartRuntime.chartJsConfig;
        this.isBarChart = chartData.type === "bar";
        this.chartId = `${chartData.type}-${this.props.chartId}`;
        this.datasetBoundaries = this.getAxisLimitsFromDataset(chartData);
        if (this.sliceable) {
            const updatedData = this.getDetailChartConfiguration(chartData);
            chartRuntime.chartJsConfig = updatedData;
        }
        super.createChart(chartRuntime);
        this.hasLinearScale = this.chart?.scales?.x?.type === "linear";
        if (!this.sliceable || !("masterChartConfig" in chartRuntime)) {
            this.isMasterChartAllowed = false;
            return;
        }
        this.masterChart?.destroy();
        const masterChartCtx = (this.masterChartCanvas?.el).getContext("2d");
        this.setMasterChartCursor(chartRuntime);
        this.masterChart = new window.Chart(masterChartCtx, this.getMasterChartConfiguration(chartRuntime["masterChartConfig"]));
        this.resetAxesLimits();
        if (this.chart?.options) {
            this.chart.options.animation = false;
        }
    }
    updateChartJs(chartRuntime) {
        const chartData = chartRuntime.chartJsConfig;
        const { min, max } = this.getAxisLimitsFromDataset(chartData);
        if (this.datasetBoundaries.min !== min || this.datasetBoundaries.max !== max) {
            this.store.clearAxisLimits(this.chartId);
            this.datasetBoundaries = { min, max };
        }
        this.isBarChart = chartData?.type === "bar";
        this.chartId = `${chartData.type}-${this.props.chartId}`;
        if (this.sliceable) {
            const updatedData = this.getDetailChartConfiguration(chartData);
            chartRuntime.chartJsConfig = updatedData;
        }
        super.updateChartJs(chartRuntime);
        this.hasLinearScale = this.chart?.scales?.x?.type === "linear";
        if (!this.sliceable || !("masterChartConfig" in chartRuntime)) {
            this.masterChart = undefined;
            this.isMasterChartAllowed = false;
        }
        else {
            const masterChartConfig = this.getMasterChartConfiguration(chartRuntime["masterChartConfig"]);
            if (!this.masterChart) {
                const masterChartCtx = this.masterChartCanvas.el.getContext("2d");
                this.masterChart = new window.Chart(masterChartCtx, masterChartConfig);
            }
            else {
                this.masterChart.data = masterChartConfig.data;
                this.masterChart.config.options = masterChartConfig.options;
                this.masterChart.update();
            }
            this.setMasterChartCursor(chartRuntime);
        }
        this.resetAxesLimits();
        if (this.chart?.options) {
            this.chart.options.animation = false;
        }
    }
    resetAxesLimits() {
        if (!this.chart) {
            return;
        }
        const storedLimits = this.store.originalAxisLimits[this.chartId]?.x;
        if (!storedLimits) {
            let scales = this.masterChart
                ? this.masterChart.scales
                : this.chart.scales;
            if (!this.hasLinearScale && scales.x) {
                scales = {
                    ...scales,
                    x: this.adjustBoundaries(scales.x),
                };
            }
            this.store.resetAxisLimits(this.chartId, scales);
            return;
        }
        this.updateTrendingLineAxes();
        this.chart.update();
        if (!this.masterChart) {
            return;
        }
        this.masterChart.update();
    }
    updateTrendingLineAxes() {
        this.store.updateTrendLineConfiguration(this.chartId);
        const limits = this.store.currentAxesLimits[this.chartId];
        for (const axisId of [TREND_LINE_XAXIS_ID, MOVING_AVERAGE_TREND_LINE_XAXIS_ID]) {
            if (!this.chart?.config?.options?.scales?.[axisId] || !limits?.[axisId]) {
                continue;
            }
            this.chart.config.options.scales[axisId].min = limits[axisId].min;
            this.chart.config.options.scales[axisId].max = limits[axisId].max;
        }
    }
    get upperBound() {
        return this.computePosition(this.store.currentAxesLimits[this.chartId]?.x?.max);
    }
    get lowerBound() {
        return this.computePosition(this.store.currentAxesLimits[this.chartId]?.x?.min);
    }
    computePosition(value) {
        if (value === undefined || !this.masterChart?.scales?.x) {
            return undefined;
        }
        const scale = this.masterChart.scales.x;
        if (this.hasLinearScale) {
            return scale.getPixelForValue(value);
        }
        if (!this.masterChart.chartArea) {
            return undefined;
        }
        const { left, right } = this.masterChart.chartArea;
        const { min, max } = scale;
        const offset = this.axisOffset;
        return left + ((right - left) * (offset + value - min)) / (2 * offset + max - min);
    }
    computeCoordinate(position) {
        if (!this.masterChart) {
            return undefined;
        }
        const scale = this.masterChart.scales.x;
        if (this.hasLinearScale) {
            const value = scale.getValueForPixel(position);
            if (value === undefined) {
                return undefined;
            }
            return Math.round(value * 100) / 100;
        }
        const { left, right } = this.masterChart.chartArea;
        const offset = this.axisOffset;
        return (scale.min -
            offset +
            ((scale.max + 2 * offset - scale.min) * (position - left)) / (right - left));
    }
    /**
     * Compute min and max from the store, adjusting them if needed for non linear scales.
     * Getting the value from the store, we have to ensure that the values are integers for
     * non linear scales (bar and category). To select a bar in the chart, we have to include
     * the whole bar, which means that for the i-th bar, the selected min should be <= i and
     * the selected max should be >= i, so using the Math.floor and Math.ceil functions is
     * the right way to do it.
     * Sometimes, we can get a minimal value > the maximal value, which arise when the user
     * select a very small area in the master chart, and hasn't selected the middle of a bar
     * or a group of bars (in case of more than one data series).
     * Assuming we have to select the middle of a bar/a groupe of bars, we will reject the
     * coming value afterward. In this case, we do not update the chart because it would lead
     * to an empty chart.
     */
    getStoredBoundaries() {
        let { min, max } = this.store.currentAxesLimits[this.chartId].x;
        if (!this.hasLinearScale) {
            min = Math.ceil(min);
            max = Math.floor(max);
        }
        return { min, max };
    }
    /**
     * Adjust the min and max values of an axis if needed for non linear scales.
     * Here, after rounding (see docstring of getStoredBoundaries), we adjust the min by
     * substracting the axis offset, and we add it to the max, because when computing from the
     * scale, chartJs use integer values as the limits for non linear scales. If we have a min
     * value of 1, it means we want to start displaying from 0.5, and if we have a max value of
     * 4, it means we want to display until 4.5.
     * Here, we don't have to check if min > max because we are computing from the scale, and
     * chartJs ensures that this won't happen, even after our adjustments.
     */
    adjustBoundaries({ min, max }) {
        if (!this.hasLinearScale) {
            min = Math.ceil(min) - this.axisOffset;
            max = Math.floor(max) + this.axisOffset;
        }
        return { min, max };
    }
    updateAxisLimits(xMin, xMax) {
        if (xMin === xMax) {
            return;
        }
        if (!this.chart) {
            return;
        }
        this.store.updateAxisLimits(this.chartId, { min: xMin, max: xMax });
        const { min, max } = this.getStoredBoundaries();
        if (max > min || (this.isBarChart && max === min)) {
            this.chart.config.options.scales.x.min = min;
            this.chart.config.options.scales.x.max = max;
            this.updateTrendingLineAxes();
            this.chart.update();
        }
        this.masterChart?.update();
    }
    onMasterChartPointerDown(ev) {
        this.removeEventListeners();
        const position = ev.offsetX;
        if (!this.masterChart?.chartArea || !this.chart?.scales?.x) {
            return;
        }
        const { left, right, top, bottom } = this.masterChart.chartArea;
        const upperBound = this.upperBound ?? right;
        const lowerBound = this.lowerBound ?? left;
        if (position < left - 5 || position > right + 5 || ev.offsetY < top || ev.offsetY > bottom) {
            return;
        }
        ev.preventDefault();
        ev.stopPropagation();
        let startingPositionOnChart, windowSize, startX;
        const startingEventPosition = ev.clientX - (this.masterChartCanvas.el?.getBoundingClientRect().left ?? 0);
        if ((lowerBound !== left || upperBound !== right) &&
            position > lowerBound + 5 &&
            position < upperBound - 5) {
            startingPositionOnChart = ev.offsetX - lowerBound;
            this.mode = "moveInMaster";
            const currentLimits = this.store.currentAxesLimits[this.chartId]?.x;
            windowSize =
                (currentLimits?.max ?? this.chart.scales.x.max) -
                    (currentLimits?.min ?? this.chart.scales.x.min);
        }
        else {
            this.mode = "selectInMaster";
            if (Math.abs(position - lowerBound) < 5) {
                startingPositionOnChart = upperBound;
            }
            else if (Math.abs(position - upperBound) < 5) {
                startingPositionOnChart = lowerBound;
            }
            else {
                startingPositionOnChart = clip(position, left, right);
            }
            startX = this.computeCoordinate(startingPositionOnChart);
        }
        const storedMin = this.store.originalAxisLimits[this.chartId].x.min;
        const storedMax = this.store.originalAxisLimits[this.chartId].x.max;
        const computeNewAxisLimits = (position) => {
            if (this.mode === "moveInMaster") {
                let min = this.computeCoordinate(position - startingPositionOnChart);
                if (min < storedMin) {
                    min = storedMin;
                }
                else if (min > storedMax - windowSize) {
                    min = storedMax - windowSize;
                }
                return { min, max: min + windowSize };
            }
            else if (this.mode === "selectInMaster") {
                const upperBound = clip(position, left, right);
                if (Math.abs(startingPositionOnChart - upperBound) > 5) {
                    const endX = this.computeCoordinate(upperBound);
                    if (startX === undefined || endX === undefined) {
                        return {};
                    }
                    return {
                        min: Math.min(startX, endX),
                        max: Math.max(startX, endX),
                    };
                }
            }
            return {};
        };
        const onMasterChartDrag = (ev) => {
            const position = ev.clientX - (this.masterChartCanvas.el?.getBoundingClientRect().left ?? 0);
            if (Math.abs(position - startingEventPosition) < 5) {
                return;
            }
            const { min, max } = computeNewAxisLimits(position);
            if (min !== undefined && max !== undefined) {
                this.updateAxisLimits(min, max);
            }
        };
        const onMasterChartPointerUp = (ev) => {
            this.removeEventListeners();
            let { min, max } = this.chart.scales.x;
            if (!this.hasLinearScale) {
                if (this.mode === "moveInMaster") {
                    min = Math.round(min) - this.axisOffset;
                    max = min + windowSize;
                }
                else {
                    ({ min, max } = this.adjustBoundaries({ min, max }));
                }
            }
            this.updateAxisLimits(min, max);
            this.mode = undefined;
        };
        this.removeEventListeners = () => {
            window.removeEventListener("pointermove", onMasterChartDrag, true);
            window.removeEventListener("pointerup", onMasterChartPointerUp, true);
        };
        window.addEventListener("pointermove", onMasterChartDrag, true);
        window.addEventListener("pointerup", onMasterChartPointerUp, true);
    }
    onMasterChartPointerMove(ev) {
        const { offsetX: x, offsetY: y, target } = ev;
        if (!target || !this.isMasterChartAllowed) {
            return;
        }
        if (this.mode === undefined) {
            if (!this.masterChart?.chartArea) {
                target["style"].cursor = "default";
                return;
            }
            const { left, right, top, bottom } = this.masterChart.chartArea;
            const start = this.lowerBound ?? left;
            const end = this.upperBound ?? right;
            if (y < top || y > bottom) {
                target["style"].cursor = "default";
            }
            else if (Math.abs(start - x) < 5 || Math.abs(end - x) < 5) {
                target["style"].cursor = "ew-resize";
            }
            else if (start < x && x < end && (start !== left || end !== right)) {
                target["style"].cursor = "grab";
            }
            else {
                target["style"].cursor = "crosshair";
            }
        }
    }
    onMasterChartMouseLeave(ev) {
        const target = ev.target;
        if (!target || !this.isMasterChartAllowed) {
            return;
        }
        target["style"].cursor = "default";
    }
    onMasterChartDoubleClick(ev) {
        this.mode = undefined;
        const position = ev.offsetX;
        if (!this.masterChart?.chartArea || !this.chart?.scales.x) {
            return;
        }
        const { left, right, top, bottom } = this.masterChart.chartArea;
        let upperBound = this.upperBound ?? right;
        let lowerBound = this.lowerBound ?? left;
        if (upperBound < lowerBound) {
            [upperBound, lowerBound] = [lowerBound, upperBound];
        }
        if (position < left - 5 || position > right + 5 || ev.offsetY < top || ev.offsetY > bottom) {
            return;
        }
        ev.preventDefault();
        ev.stopPropagation();
        let { min, max } = this.store.currentAxesLimits[this.chartId]?.x ?? this.chart.scales.x;
        const originalAxisLimits = this.store.originalAxisLimits[this.chartId].x;
        if (!originalAxisLimits) {
            return;
        }
        if (Math.abs(position - lowerBound) < 5) {
            min = originalAxisLimits.min;
        }
        else if (Math.abs(position - upperBound) < 5) {
            max = originalAxisLimits.max;
        }
        else if (lowerBound < position && position < upperBound) {
            min = originalAxisLimits.min;
            max = originalAxisLimits.max;
        }
        else {
            return;
        }
        this.updateAxisLimits(min, max);
    }
}

const cellAnimationRegistry = new Registry();
cellAnimationRegistry.add("animatedBackgroundColorChange", {
    id: "animatedBackgroundColorChange",
    easingFn: "easeOutCubic",
    hasAnimation: (oldBox, newBox) => {
        return oldBox?.style?.fillColor !== newBox?.style?.fillColor;
    },
    updateAnimation: function (progress, animatedBox, oldBox, newBox) {
        const colorScale = getColorScale([
            { value: 0, color: oldBox.style.fillColor || "#ffffff" },
            { value: 1, color: newBox.style.fillColor || "#ffffff" },
        ]);
        animatedBox.style.fillColor = colorScale(EASING_FN[this.easingFn](progress));
    },
});
cellAnimationRegistry.add("animatedTextColorChange", {
    id: "animatedTextColorChange",
    easingFn: "easeOutCubic",
    hasAnimation: (oldBox, newBox) => {
        return oldBox?.style?.textColor !== newBox?.style?.textColor;
    },
    updateAnimation: function (progress, animatedBox, oldBox, newBox) {
        const colorScale = getColorScale([
            { value: 0, color: oldBox.style.textColor || "#000000" },
            { value: 1, color: newBox.style.textColor || "#000000" },
        ]);
        animatedBox.style.textColor = colorScale(EASING_FN[this.easingFn](progress));
    },
});
cellAnimationRegistry.add("animatedDataBar", {
    id: "animatedDataBar",
    easingFn: "easeOutCubic",
    hasAnimation: (oldBox, newBox) => {
        return oldBox?.dataBarFill?.percentage !== newBox?.dataBarFill?.percentage;
    },
    updateAnimation: function (progress, animatedBox, oldBox, newBox) {
        const startingPercentage = oldBox?.dataBarFill?.percentage || 0;
        const endingPercentage = newBox?.dataBarFill?.percentage || 0;
        const value = EASING_FN[this.easingFn](progress);
        const percentage = startingPercentage + (endingPercentage - startingPercentage) * value;
        animatedBox.dataBarFill = {
            color: newBox.dataBarFill?.color || oldBox.dataBarFill?.color || "#ffffff",
            percentage: percentage,
        };
    },
});
cellAnimationRegistry.add("textFadeIn", {
    id: "textFadeIn",
    easingFn: "easeInCubic",
    hasAnimation: (oldBox, newBox) => {
        const oldText = oldBox?.content?.textLines?.join("\n");
        const newText = newBox?.content?.textLines?.join("\n");
        return Boolean(!oldText && newText);
    },
    updateAnimation: function (progress, animatedBox, oldBox, newBox) {
        animatedBox.textOpacity = EASING_FN[this.easingFn](progress);
    },
});
cellAnimationRegistry.add("textFadeOut", {
    id: "textFadeOut",
    easingFn: "easeOutCubic",
    hasAnimation: (oldBox, newBox) => {
        const oldText = oldBox?.content?.textLines?.join("\n");
        const newText = newBox?.content?.textLines?.join("\n");
        return Boolean(oldText && !newText);
    },
    updateAnimation: function (progress, animatedBox, oldBox, newBox) {
        const textOpacity = 1 - EASING_FN[this.easingFn](progress);
        const style = { ...oldBox.style };
        delete style.fillColor;
        animatedBox.textOpacity = textOpacity;
        animatedBox.content = oldBox.content;
        animatedBox.clipRect = oldBox.clipRect;
        Object.assign(animatedBox.style, style);
    },
});
cellAnimationRegistry.add("iconFadeIn", {
    id: "iconFadeIn",
    easingFn: "easeInCubic",
    hasAnimation: (oldBox, newBox) => {
        return Boolean((!oldBox?.icons?.left && newBox?.icons?.left) ||
            (!oldBox?.icons?.right && newBox?.icons?.right) ||
            (!oldBox?.icons?.center && newBox?.icons?.center));
    },
    updateAnimation: function (progress, animatedBox, oldBox, newBox) {
        const iconOpacity = EASING_FN[this.easingFn](progress);
        if (animatedBox.icons.left && newBox.icons.left && !oldBox.icons.left) {
            animatedBox.icons.left.opacity = iconOpacity;
        }
        if (animatedBox.icons.right && newBox.icons.right && !oldBox.icons.right) {
            animatedBox.icons.right.opacity = iconOpacity;
        }
        if (animatedBox.icons.center && newBox.icons.center && !oldBox.icons.center) {
            animatedBox.icons.center.opacity = iconOpacity;
        }
    },
});
cellAnimationRegistry.add("iconFadeOut", {
    id: "iconFadeOut",
    easingFn: "easeOutCubic",
    hasAnimation: (oldBox, newBox) => {
        return Boolean((oldBox?.icons?.left && !newBox?.icons?.left) ||
            (oldBox?.icons?.right && !newBox?.icons?.right) ||
            (oldBox?.icons?.center && !newBox?.icons?.center));
    },
    updateAnimation: function (progress, animatedBox, oldBox, newBox) {
        const iconOpacity = 1 - EASING_FN[this.easingFn](progress);
        if (!animatedBox.icons) {
            animatedBox.icons = {};
        }
        if (oldBox.icons.left && !newBox.icons.left) {
            animatedBox.icons.left = { ...oldBox.icons.left, opacity: iconOpacity };
        }
        if (oldBox.icons.right && !newBox.icons.right) {
            animatedBox.icons.right = { ...oldBox.icons.right, opacity: iconOpacity };
        }
        if (oldBox.icons.center && !newBox.icons.center) {
            animatedBox.icons.center = { ...oldBox.icons.center, opacity: iconOpacity };
        }
    },
});
cellAnimationRegistry.add("textChange", {
    id: "textChange",
    easingFn: "easeOutCubic",
    hasAnimation: (oldBox, newBox) => {
        const oldText = oldBox?.content?.textLines?.join("\n");
        const newText = newBox?.content?.textLines?.join("\n");
        // Note: here, we also animate changes to icons layout (margins/size change, or icon appearing/disappearing)
        // because a change to the icon layout will impact where the text is positioned.
        return Boolean(oldText && newText && (oldText !== newText || hasIconLayoutChange(newBox, oldBox)));
    },
    updateAnimation: function (progress, animatedBox, oldBox, newBox) {
        const value = EASING_FN[this.easingFn](progress);
        const slideInY = newBox.y + (value - 1) * newBox.height;
        const slideOutY = newBox.y + value * newBox.height;
        const iconLayoutChange = hasIconLayoutChange(newBox, oldBox);
        const slideInBox = {
            id: newBox.id + "-text-slide-in",
            x: newBox.x,
            y: slideInY,
            width: newBox.width,
            height: newBox.height,
            style: { ...newBox.style },
            skipCellGridLines: true,
            content: newBox.content ? { ...newBox.content } : undefined,
            clipRect: newBox.clipRect || {
                ...newBox,
                // large width to avoid clipping the text it it didn't have a clipRect before,
                // we mainly want to clip the Y for the animation
                x: Math.max(0, newBox.x - (newBox.content?.width || 0)),
                width: newBox.width + (newBox.content?.width || 0) * 2,
            },
            icons: iconLayoutChange
                ? addClipRectToIcons(newBox.icons, newBox)
                : makeIconsEmpty(newBox.icons),
        };
        const slideOutBox = {
            id: oldBox.id + "-text-slide-out",
            x: newBox.x,
            y: slideOutY,
            width: newBox.width,
            height: newBox.height,
            style: { ...oldBox.style },
            skipCellGridLines: true,
            content: oldBox.content ? { ...oldBox.content } : undefined,
            clipRect: oldBox.clipRect || {
                ...newBox,
                x: Math.max(0, newBox.x - (oldBox.content?.width || 0)),
                width: newBox.width + (oldBox.content?.width || 0) * 2,
            },
            icons: iconLayoutChange
                ? addClipRectToIcons(oldBox.icons, newBox)
                : makeIconsEmpty(oldBox.icons),
        };
        if (newBox.content && oldBox.content && slideInBox.content && slideOutBox.content) {
            const slideInContentY = newBox.content.y + (value - 1) * newBox.height;
            const slideOutContentY = newBox.content.y + value * newBox.height;
            slideInBox.content.y = slideInContentY;
            slideOutBox.content.y = slideOutContentY;
        }
        slideOutBox.style.fillColor = slideInBox.style.fillColor = undefined;
        animatedBox.content = undefined;
        animatedBox.icons = iconLayoutChange ? {} : animatedBox.icons;
        return { newBoxes: [slideInBox, slideOutBox] };
    },
});
cellAnimationRegistry.add("borderFadeIn", {
    id: "borderFadeIn",
    easingFn: "easeInCubic",
    hasAnimation: (oldBox, newBox) => {
        return Boolean((!oldBox?.border?.bottom && newBox?.border?.bottom) ||
            (!oldBox?.border?.top && newBox?.border?.top) ||
            (!oldBox?.border?.left && newBox?.border?.left) ||
            (!oldBox?.border?.right && newBox?.border?.right));
    },
    updateAnimation: function (progress, animatedBox, oldBox, newBox) {
        const borderOpacity = EASING_FN[this.easingFn](progress);
        if (animatedBox.border?.top && newBox.border?.top && !oldBox.border?.top) {
            animatedBox.border.top.opacity = borderOpacity;
        }
        if (animatedBox.border?.bottom && newBox.border?.bottom && !oldBox.border?.bottom) {
            animatedBox.border.bottom.opacity = borderOpacity;
        }
        if (animatedBox.border?.left && newBox.border?.left && !oldBox.border?.left) {
            animatedBox.border.left.opacity = borderOpacity;
        }
        if (animatedBox.border?.right && newBox.border?.right && !oldBox.border?.right) {
            animatedBox.border.right.opacity = borderOpacity;
        }
    },
});
cellAnimationRegistry.add("borderFadeOut", {
    id: "borderFadeOut",
    easingFn: "easeOutCubic",
    hasAnimation: (oldBox, newBox) => {
        return Boolean((oldBox?.border?.bottom && !newBox?.border?.bottom) ||
            (oldBox?.border?.top && !newBox?.border?.top) ||
            (oldBox?.border?.left && !newBox?.border?.left) ||
            (oldBox?.border?.right && !newBox?.border?.right));
    },
    updateAnimation: function (progress, animatedBox, oldBox, newBox) {
        const borderOpacity = 1 - EASING_FN[this.easingFn](progress);
        if (!animatedBox.border) {
            animatedBox.border = {};
        }
        if (oldBox.border?.top && !newBox.border?.top) {
            animatedBox.border.top = { ...oldBox.border.top, opacity: borderOpacity };
        }
        if (oldBox.border?.bottom && !newBox.border?.bottom) {
            animatedBox.border.bottom = { ...oldBox.border.bottom, opacity: borderOpacity };
        }
        if (oldBox.border?.left && !newBox.border?.left) {
            animatedBox.border.left = { ...oldBox.border.left, opacity: borderOpacity };
        }
        if (oldBox.border?.right && !newBox.border?.right) {
            animatedBox.border.right = { ...oldBox.border.right, opacity: borderOpacity };
        }
    },
});
cellAnimationRegistry.add("borderColorChange", {
    id: "borderColorChange",
    easingFn: "easeOutCubic",
    hasAnimation: (oldBox, newBox) => {
        const oldBorder = oldBox?.border;
        const newBorder = newBox?.border;
        if (!oldBorder || !newBorder) {
            return false;
        }
        return Boolean(oldBorder.bottom?.color !== newBorder.bottom?.color ||
            oldBorder.top?.color !== newBorder.top?.color ||
            oldBorder.left?.color !== newBorder.left?.color ||
            oldBorder.right?.color !== newBorder.right?.color);
    },
    updateAnimation: function (progress, animatedBox, oldBox, newBox) {
        const animateBorderColor = (side) => {
            const oldBorder = oldBox?.border?.[side];
            const newBorder = newBox?.border?.[side];
            const animatedBorder = animatedBox.border?.[side];
            if (oldBorder && newBorder && animatedBorder) {
                const colorScale = getColorScale([
                    { value: 0, color: oldBorder.color || "#000000" },
                    { value: 1, color: newBorder.color || "#000000" },
                ]);
                animatedBorder.color = colorScale(EASING_FN[this.easingFn](progress));
            }
        };
        animateBorderColor("top");
        animateBorderColor("bottom");
        animateBorderColor("left");
        animateBorderColor("right");
    },
});
cellAnimationRegistry.add("iconChange", {
    id: "iconChange",
    easingFn: "easeOutCubic",
    hasAnimation: (oldBox, newBox) => {
        return (!hasIconLayoutChange(newBox, oldBox) &&
            Boolean(oldBox?.icons?.center?.svg?.name !== newBox?.icons?.center?.svg?.name ||
                oldBox?.icons?.left?.svg?.name !== newBox?.icons?.left?.svg?.name ||
                oldBox?.icons?.right?.svg?.name !== newBox?.icons?.right?.svg?.name));
    },
    updateAnimation: function (progress, animatedBox, oldBox, newBox) {
        const value = EASING_FN[this.easingFn](progress);
        const slideInY = newBox.y + (value - 1) * newBox.height;
        const slideOutY = newBox.y + value * newBox.height;
        const newBoxes = [];
        const animateIconChange = (side) => {
            const oldIcon = oldBox.icons?.[side];
            const newIcon = newBox.icons?.[side];
            const slideInBox = {
                id: `${newBox.id}-icon-${side}-slide-in`,
                style: { verticalAlign: newBox.style.verticalAlign },
                x: newBox.x,
                y: slideInY,
                width: newBox.width,
                height: newBox.height,
                skipCellGridLines: true,
                icons: { [side]: { ...newIcon, clipRect: newBox } },
            };
            const slideOutBox = {
                id: `${newBox.id}-icon-${side}-slide-out`,
                style: { verticalAlign: oldBox.style.verticalAlign },
                x: newBox.x,
                y: slideOutY,
                width: newBox.width,
                height: newBox.height,
                skipCellGridLines: true,
                icons: { [side]: { ...oldIcon, clipRect: newBox } },
            };
            animatedBox.icons[side] = makeIconsEmpty(newBox.icons)[side];
            newBoxes.push(slideInBox, slideOutBox);
        };
        animateIconChange("left");
        animateIconChange("right");
        animateIconChange("center");
        return { newBoxes };
    },
});
const EASING_FN = {
    linear: (t) => t,
    easeInCubic: (t) => t * t * t,
    easeOutCubic: (t) => (t -= 1) * t * t + 1,
    easeInOutCubic: (t) => ((t /= 0.5) < 1 ? 0.5 * t * t * t : 0.5 * ((t -= 2) * t * t + 2)),
    easeOutQuart: (t) => -((t -= 1) * t * t * t - 1),
};
function makeIconsEmpty(icons) {
    return {
        left: icons.left ? { ...icons.left, svg: undefined } : undefined,
        right: icons.right ? { ...icons.right, svg: undefined } : undefined,
        center: icons.center ? { ...icons.center, svg: undefined } : undefined,
    };
}
function addClipRectToIcons(icons, clipRect) {
    return {
        left: icons.left ? { ...icons.left, clipRect } : undefined,
        right: icons.right ? { ...icons.right, clipRect } : undefined,
        center: icons.center ? { ...icons.center, clipRect } : undefined,
    };
}
/**
 *  Check if the icons have appeared, disappeared or changed margin/size/align. Those changes affect where the text is positioned.
 */
function hasIconLayoutChange(newBox, oldBox) {
    const hasLayoutChange = (newIcon, oldIcon) => {
        if (oldIcon && newIcon) {
            return !!(newIcon.horizontalAlign !== oldIcon.horizontalAlign ||
                newIcon.size !== oldIcon.size ||
                newIcon.margin !== oldIcon.margin ||
                (newIcon.svg && !oldIcon.svg) ||
                (!newIcon.svg && oldIcon.svg));
        }
        return !!((newIcon && !oldIcon) || (!newIcon && oldIcon));
    };
    return (hasLayoutChange(newBox?.icons.left, oldBox?.icons.left) ||
        hasLayoutChange(newBox?.icons.right, oldBox?.icons.right) ||
        hasLayoutChange(newBox?.icons.center, oldBox?.icons.center));
}

const ANIMATION_DURATION = 1000;
class GaugeChartComponent extends Component {
    static template = "o-spreadsheet-GaugeChartComponent";
    static props = {
        chartId: String,
        isFullScreen: { type: Boolean, optional: true },
    };
    canvas = useRef("chartContainer");
    animationStore;
    get runtime() {
        return this.env.model.getters.getChartRuntime(this.props.chartId);
    }
    setup() {
        if (this.env.model.getters.isDashboard()) {
            this.animationStore = useStore(ChartAnimationStore);
        }
        let animation = null;
        let lastRuntime = undefined;
        useEffect(() => {
            if (this.env.isDashboard() &&
                lastRuntime === undefined && // first render
                this.animationStore?.animationPlayed[this.animationChartId] !== "gauge") {
                animation = this.drawGaugeWithAnimation();
                this.animationStore?.disableAnimationForChart(this.animationChartId, "gauge");
            }
            else if (this.env.isDashboard() &&
                lastRuntime !== undefined && // not first render
                !deepEquals(this.runtime, lastRuntime)) {
                animation = this.drawGaugeWithAnimation();
                this.animationStore?.disableAnimationForChart(this.animationChartId, "gauge");
            }
            else {
                drawGaugeChart(this.canvasEl, this.runtime);
            }
            lastRuntime = this.runtime;
            return () => animation?.stop();
        }, () => {
            const rect = this.canvasEl.getBoundingClientRect();
            return [rect.width, rect.height, this.runtime, this.canvas.el, window.devicePixelRatio];
        });
    }
    drawGaugeWithAnimation() {
        drawGaugeChart(this.canvasEl, { ...this.runtime, animationValue: 0 });
        const gaugeValue = this.runtime.gaugeValue?.value || 0;
        const upperBound = this.runtime.maxValue.value;
        const finalValue = Math.sign(gaugeValue) * Math.min(Math.abs(gaugeValue), Math.abs(upperBound));
        if (finalValue === 0) {
            return null;
        }
        const lowerBound = this.runtime.minValue.value;
        const animation = new Animation(lowerBound, finalValue, ANIMATION_DURATION, (animationValue) => drawGaugeChart(this.canvasEl, { ...this.runtime, animationValue }));
        animation.start();
        return animation;
    }
    get canvasEl() {
        return this.canvas.el;
    }
    get animationChartId() {
        return this.props.isFullScreen ? this.props.chartId + "-fullscreen" : this.props.chartId;
    }
}
/**
 * Animation interpolating values using the ease-out quartic curve function (chartJS default easing)
 */
class Animation {
    startValue;
    endValue;
    duration;
    callback;
    startTime = undefined;
    animationFrameId = null;
    constructor(startValue, endValue, duration, callback) {
        this.startValue = startValue;
        this.endValue = endValue;
        this.duration = duration;
        this.callback = callback;
    }
    start() {
        this.animationFrameId = requestAnimationFrame(this.animate.bind(this));
    }
    stop() {
        if (this.animationFrameId) {
            cancelAnimationFrame(this.animationFrameId);
            this.animationFrameId = null;
        }
    }
    animate(timestamp) {
        if (!this.startTime) {
            this.startTime = timestamp;
        }
        const elapsed = timestamp - this.startTime;
        const progress = Math.min(elapsed / this.duration, 1);
        const currentValue = this.startValue + (this.endValue - this.startValue) * EASING_FN.easeOutQuart(progress);
        this.callback(currentValue);
        if (progress < 1) {
            this.animationFrameId = requestAnimationFrame(this.animate.bind(this));
        }
        else {
            this.stop();
        }
    }
}

class ComboChart extends AbstractChart {
    dataSets;
    labelRange;
    background;
    legendPosition;
    aggregated;
    dataSetsHaveTitle;
    dataSetDesign;
    axesDesign;
    type = "combo";
    showValues;
    hideDataMarkers;
    zoomable;
    constructor(definition, sheetId, getters) {
        super(definition, sheetId, getters);
        this.dataSets = createDataSets(getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);
        this.labelRange = createValidRange(getters, sheetId, definition.labelRange);
        this.background = definition.background;
        this.legendPosition = definition.legendPosition;
        this.aggregated = definition.aggregated;
        this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
        this.dataSetDesign = definition.dataSets;
        this.axesDesign = definition.axesDesign;
        this.showValues = definition.showValues;
        this.hideDataMarkers = definition.hideDataMarkers;
        this.zoomable = definition.zoomable;
    }
    static transformDefinition(chartSheetId, definition, applyChange) {
        return transformChartDefinitionWithDataSetsWithZone(chartSheetId, definition, applyChange);
    }
    static validateChartDefinition(validator, definition) {
        return validator.checkValidations(definition, checkDataset, checkLabelRange);
    }
    getContextCreation() {
        const range = [];
        for (const [i, dataSet] of this.dataSets.entries()) {
            range.push({
                ...this.dataSetDesign?.[i],
                dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId),
            });
        }
        return {
            ...this,
            range,
            auxiliaryRange: this.labelRange
                ? this.getters.getRangeString(this.labelRange, this.sheetId)
                : undefined,
        };
    }
    getDefinition() {
        return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);
    }
    getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {
        const ranges = [];
        for (const [i, dataSet] of dataSets.entries()) {
            ranges.push({
                ...this.dataSetDesign?.[i],
                dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId),
                type: this.dataSetDesign?.[i]?.type ?? (i ? "line" : "bar"),
            });
        }
        return {
            type: "combo",
            dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,
            background: this.background,
            dataSets: ranges,
            legendPosition: this.legendPosition,
            labelRange: labelRange
                ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)
                : undefined,
            title: this.title,
            aggregated: this.aggregated,
            axesDesign: this.axesDesign,
            showValues: this.showValues,
            hideDataMarkers: this.hideDataMarkers,
            zoomable: this.zoomable,
            humanize: this.humanize,
        };
    }
    getDefinitionForExcel() {
        const { dataSets, labelRange } = this.getCommonDataSetAttributesForExcel(this.labelRange, this.dataSets, shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle));
        const definition = this.getDefinition();
        return {
            ...definition,
            backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),
            fontColor: toXlsxHexColor(chartFontColor(this.background)),
            dataSets,
            labelRange,
            verticalAxis: getDefinedAxis(definition),
        };
    }
    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 ComboChart(definition, this.sheetId, this.getters);
    }
    static getDefinitionFromContextCreation(context) {
        const dataSets = (context.range ?? []).map((ds, index) => ({
            ...ds,
            type: index ? "line" : "bar",
        }));
        return {
            background: context.background,
            dataSets,
            dataSetsHaveTitle: context.dataSetsHaveTitle ?? false,
            aggregated: context.aggregated,
            legendPosition: context.legendPosition ?? "top",
            title: context.title || { text: "" },
            labelRange: context.auxiliaryRange || undefined,
            type: "combo",
            axesDesign: context.axesDesign,
            showValues: context.showValues,
            hideDataMarkers: context.hideDataMarkers,
            zoomable: context.zoomable,
            humanize: context.humanize,
        };
    }
    duplicateInDuplicatedSheet(newSheetId) {
        const dataSets = duplicateDataSetsInDuplicatedSheet(this.sheetId, newSheetId, this.dataSets);
        const labelRange = duplicateLabelRangeInDuplicatedSheet(this.sheetId, newSheetId, this.labelRange);
        const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, newSheetId);
        return new ComboChart(definition, newSheetId, this.getters);
    }
    copyInSheetId(sheetId) {
        const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);
        return new ComboChart(definition, sheetId, this.getters);
    }
}
function createComboChartRuntime(chart, getters) {
    const definition = chart.getDefinition();
    const chartData = getBarChartData(definition, chart.dataSets, chart.labelRange, getters);
    const config = {
        type: "bar",
        data: {
            labels: chartData.labels,
            datasets: getComboChartDatasets(definition, chartData),
        },
        options: {
            ...CHART_COMMON_OPTIONS,
            layout: getChartLayout(definition, chartData),
            scales: getBarChartScales(definition, chartData),
            plugins: {
                title: getChartTitle(definition, getters),
                legend: getComboChartLegend(definition),
                tooltip: getBarChartTooltip(definition, chartData),
                chartShowValuesPlugin: getChartShowValues(definition, chartData),
            },
        },
    };
    return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
}

class FunnelChart extends AbstractChart {
    dataSets;
    labelRange;
    background;
    legendPosition;
    aggregated;
    type = "funnel";
    dataSetsHaveTitle;
    dataSetDesign;
    axesDesign;
    horizontal = true;
    showValues;
    funnelColors;
    cumulative;
    constructor(definition, sheetId, getters) {
        super(definition, sheetId, getters);
        this.dataSets = createDataSets(getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);
        this.labelRange = createValidRange(getters, sheetId, definition.labelRange);
        this.background = definition.background;
        this.legendPosition = definition.legendPosition;
        this.aggregated = definition.aggregated;
        this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
        this.dataSetDesign = definition.dataSets;
        this.axesDesign = definition.axesDesign;
        this.showValues = definition.showValues;
        this.horizontal = true;
        this.funnelColors = definition.funnelColors;
        this.cumulative = definition.cumulative;
    }
    static transformDefinition(chartSheetId, definition, applyChange) {
        return transformChartDefinitionWithDataSetsWithZone(chartSheetId, definition, applyChange);
    }
    static validateChartDefinition(validator, definition) {
        return validator.checkValidations(definition, checkDataset, checkLabelRange);
    }
    static getDefinitionFromContextCreation(context) {
        return {
            background: context.background,
            dataSets: context.range ?? [],
            dataSetsHaveTitle: context.dataSetsHaveTitle ?? false,
            aggregated: context.aggregated ?? false,
            legendPosition: "none",
            title: context.title || { text: "" },
            type: "funnel",
            labelRange: context.auxiliaryRange || undefined,
            showValues: context.showValues,
            axesDesign: context.axesDesign,
            funnelColors: context.funnelColors,
            horizontal: true,
            cumulative: context.cumulative,
            humanize: context.humanize,
        };
    }
    getContextCreation() {
        const range = [];
        for (const [i, dataSet] of this.dataSets.entries()) {
            range.push({
                ...this.dataSetDesign?.[i],
                dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId),
            });
        }
        return {
            ...this,
            range,
            auxiliaryRange: this.labelRange
                ? this.getters.getRangeString(this.labelRange, this.sheetId)
                : undefined,
        };
    }
    duplicateInDuplicatedSheet(newSheetId) {
        const dataSets = duplicateDataSetsInDuplicatedSheet(this.sheetId, newSheetId, this.dataSets);
        const labelRange = duplicateLabelRangeInDuplicatedSheet(this.sheetId, newSheetId, this.labelRange);
        const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, newSheetId);
        return new FunnelChart(definition, newSheetId, this.getters);
    }
    copyInSheetId(sheetId) {
        const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);
        return new FunnelChart(definition, sheetId, this.getters);
    }
    getDefinition() {
        return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);
    }
    getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {
        const ranges = [];
        for (const [i, dataSet] of dataSets.entries()) {
            ranges.push({
                ...this.dataSetDesign?.[i],
                dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId),
            });
        }
        return {
            type: "funnel",
            dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,
            background: this.background,
            dataSets: ranges,
            legendPosition: this.legendPosition,
            labelRange: labelRange
                ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)
                : undefined,
            title: this.title,
            aggregated: this.aggregated,
            horizontal: this.horizontal,
            axesDesign: this.axesDesign,
            showValues: this.showValues,
            funnelColors: this.funnelColors,
            cumulative: this.cumulative,
            humanize: this.humanize,
        };
    }
    getDefinitionForExcel() {
        return 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 FunnelChart(definition, this.sheetId, this.getters);
    }
}
function createFunnelChartRuntime(chart, getters) {
    const definition = chart.getDefinition();
    const chartData = getFunnelChartData(definition, chart.dataSets, chart.labelRange, getters);
    const config = {
        type: "funnel",
        data: {
            labels: chartData.labels,
            datasets: getFunnelChartDatasets(definition, chartData),
        },
        options: {
            ...CHART_COMMON_OPTIONS,
            indexAxis: "y",
            layout: getChartLayout(definition, chartData),
            scales: getFunnelChartScales(definition, chartData),
            plugins: {
                title: getChartTitle(definition, getters),
                legend: { display: false },
                tooltip: getFunnelChartTooltip(definition, chartData),
                chartShowValuesPlugin: getChartShowValues(definition, chartData),
            },
        },
    };
    return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
}

function isDataRangeValid(definition) {
    return definition.dataRange && !rangeReference.test(definition.dataRange)
        ? "InvalidGaugeDataRange" /* CommandResult.InvalidGaugeDataRange */
        : "Success" /* CommandResult.Success */;
}
function checkRangeLimits(check, batchValidations) {
    return batchValidations((definition) => {
        if (definition.sectionRule) {
            return check(definition.sectionRule.rangeMin, "rangeMin");
        }
        return "Success" /* CommandResult.Success */;
    }, (definition) => {
        if (definition.sectionRule) {
            return check(definition.sectionRule.rangeMax, "rangeMax");
        }
        return "Success" /* CommandResult.Success */;
    });
}
function checkInflectionPointsValue(check, batchValidations) {
    return batchValidations((definition) => {
        if (definition.sectionRule) {
            return check(definition.sectionRule.lowerInflectionPoint.value, "lowerInflectionPointValue");
        }
        return "Success" /* CommandResult.Success */;
    }, (definition) => {
        if (definition.sectionRule) {
            return check(definition.sectionRule.upperInflectionPoint.value, "upperInflectionPointValue");
        }
        return "Success" /* CommandResult.Success */;
    });
}
function checkEmpty(value, valueName) {
    if (value === "") {
        switch (valueName) {
            case "rangeMin":
                return "EmptyGaugeRangeMin" /* CommandResult.EmptyGaugeRangeMin */;
            case "rangeMax":
                return "EmptyGaugeRangeMax" /* CommandResult.EmptyGaugeRangeMax */;
        }
    }
    return "Success" /* CommandResult.Success */;
}
function checkValueIsNumberOrFormula(value, valueName) {
    if (value.startsWith("=")) {
        return "Success" /* CommandResult.Success */;
    }
    if (isNaN(value)) {
        switch (valueName) {
            case "rangeMin":
                return "GaugeRangeMinNaN" /* CommandResult.GaugeRangeMinNaN */;
            case "rangeMax":
                return "GaugeRangeMaxNaN" /* CommandResult.GaugeRangeMaxNaN */;
            case "lowerInflectionPointValue":
                return "GaugeLowerInflectionPointNaN" /* CommandResult.GaugeLowerInflectionPointNaN */;
            case "upperInflectionPointValue":
                return "GaugeUpperInflectionPointNaN" /* CommandResult.GaugeUpperInflectionPointNaN */;
        }
    }
    return "Success" /* CommandResult.Success */;
}
class GaugeChart extends AbstractChart {
    dataRange;
    sectionRule;
    background;
    type = "gauge";
    constructor(definition, sheetId, getters) {
        super(definition, sheetId, getters);
        this.dataRange = 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(checkValueIsNumberOrFormula, validator.batchValidations)), validator.chainValidations(checkInflectionPointsValue(checkValueIsNumberOrFormula, validator.batchValidations)));
    }
    static transformDefinition(chartSheetId, definition, applyChange) {
        let dataRange;
        if (definition.dataRange) {
            const { changeType, range: adaptedRange } = adaptStringRange(chartSheetId, definition.dataRange, applyChange);
            if (changeType !== "REMOVE") {
                dataRange = adaptedRange;
            }
        }
        const adaptFormula = (formula) => adaptFormulaStringRanges(chartSheetId, formula, applyChange);
        const sectionRule = adaptSectionRuleFormulas(definition.sectionRule, adaptFormula);
        return {
            ...definition,
            dataRange,
            sectionRule,
        };
    }
    static getDefinitionFromContextCreation(context) {
        return {
            background: context.background,
            title: context.title || { text: "" },
            type: "gauge",
            dataRange: context.range?.[0]?.dataRange,
            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",
                    operator: "<=",
                },
                upperInflectionPoint: {
                    type: "percentage",
                    value: "40",
                    operator: "<=",
                },
            },
            humanize: context.humanize,
        };
    }
    duplicateInDuplicatedSheet(newSheetId) {
        const dataRange = duplicateLabelRangeInDuplicatedSheet(this.sheetId, newSheetId, this.dataRange);
        const adaptFormula = (formula) => this.getters.copyFormulaStringForSheet(this.sheetId, newSheetId, formula, "moveReference");
        const sectionRule = adaptSectionRuleFormulas(this.sectionRule, adaptFormula);
        const definition = this.getDefinitionWithSpecificRanges(dataRange, sectionRule, newSheetId);
        return new GaugeChart(definition, newSheetId, this.getters);
    }
    copyInSheetId(sheetId) {
        const adaptFormula = (formula) => this.getters.copyFormulaStringForSheet(this.sheetId, sheetId, formula, "keepSameReference");
        const sectionRule = adaptSectionRuleFormulas(this.sectionRule, adaptFormula);
        const definition = this.getDefinitionWithSpecificRanges(this.dataRange, sectionRule, sheetId);
        return new GaugeChart(definition, sheetId, this.getters);
    }
    getDefinition() {
        return this.getDefinitionWithSpecificRanges(this.dataRange, this.sectionRule);
    }
    getDefinitionWithSpecificRanges(dataRange, sectionRule, targetSheetId) {
        return {
            background: this.background,
            sectionRule: sectionRule,
            title: this.title,
            type: "gauge",
            dataRange: dataRange
                ? this.getters.getRangeString(dataRange, targetSheetId || this.sheetId)
                : undefined,
            humanize: this.humanize,
        };
    }
    getDefinitionForExcel() {
        // This kind of graph is not exportable in Excel
        return undefined;
    }
    getContextCreation() {
        return {
            ...this,
            range: this.dataRange
                ? [{ dataRange: this.getters.getRangeString(this.dataRange, this.sheetId) }]
                : undefined,
        };
    }
    updateRanges({ applyChange, adaptFormulaString }) {
        const dataRange = adaptChartRange(this.dataRange, applyChange);
        const adaptFormula = (formula) => adaptFormulaString(this.sheetId, formula);
        const sectionRule = adaptSectionRuleFormulas(this.sectionRule, adaptFormula);
        const definition = this.getDefinitionWithSpecificRanges(dataRange, sectionRule);
        return new GaugeChart(definition, this.sheetId, this.getters);
    }
}
function createGaugeChartRuntime(chart, getters) {
    const locale = getters.getLocale();
    const chartColors = chart.sectionRule.colors;
    let gaugeValue = undefined;
    let formattedValue = undefined;
    let format = undefined;
    const dataRange = chart.dataRange;
    if (dataRange !== undefined) {
        const cell = getters.getEvaluatedCell({
            sheetId: dataRange.sheetId,
            col: dataRange.zone.left,
            row: dataRange.zone.top,
        });
        if (cell.type === CellValueType.number) {
            gaugeValue = cell.value;
            formattedValue = cell.formattedValue;
            format = cell.format;
        }
    }
    let minValue = getFormulaNumberValue(chart.sheetId, chart.sectionRule.rangeMin, getters);
    let maxValue = getFormulaNumberValue(chart.sheetId, chart.sectionRule.rangeMax, getters);
    if (minValue === undefined || maxValue === undefined) {
        return getInvalidGaugeRuntime(chart, getters);
    }
    if (maxValue < minValue) {
        [minValue, maxValue] = [maxValue, minValue];
    }
    const lowerPoint = chart.sectionRule.lowerInflectionPoint;
    const upperPoint = chart.sectionRule.upperInflectionPoint;
    const lowerPointValue = getSectionThresholdValue(chart.sheetId, chart.sectionRule.lowerInflectionPoint, minValue, maxValue, getters);
    const upperPointValue = getSectionThresholdValue(chart.sheetId, chart.sectionRule.upperInflectionPoint, minValue, maxValue, getters);
    const inflectionValues = [];
    const colors = [];
    if (lowerPointValue !== undefined) {
        inflectionValues.push({
            value: lowerPointValue,
            label: formatOrHumanizeValue(lowerPointValue, format, locale, chart.humanize),
            operator: lowerPoint.operator,
        });
        colors.push(chartColors.lowerColor);
    }
    if (upperPointValue !== undefined && upperPointValue !== lowerPointValue) {
        inflectionValues.push({
            value: upperPointValue,
            label: formatOrHumanizeValue(upperPointValue, format, locale, chart.humanize),
            operator: upperPoint.operator,
        });
        colors.push(chartColors.middleColor);
    }
    if (upperPointValue !== undefined &&
        lowerPointValue !== undefined &&
        lowerPointValue > upperPointValue) {
        inflectionValues.reverse();
        colors.reverse();
    }
    colors.push(chartColors.upperColor);
    return {
        background: getters.getStyleOfSingleCellChart(chart.background, dataRange).background,
        title: {
            ...chart.title,
            text: chart.title.text ? getters.dynamicTranslate(chart.title.text) : "",
        },
        minValue: {
            value: minValue,
            label: formatOrHumanizeValue(minValue, format, locale, chart.humanize),
        },
        maxValue: {
            value: maxValue,
            label: formatOrHumanizeValue(maxValue, format, locale, chart.humanize),
        },
        gaugeValue: gaugeValue !== undefined && formattedValue
            ? {
                value: gaugeValue,
                label: chart.humanize
                    ? humanizeNumber({ value: gaugeValue, format }, locale)
                    : formattedValue,
            }
            : undefined,
        inflectionValues,
        colors,
    };
}
function getSectionThresholdValue(sheetId, threshold, minValue, maxValue, getters) {
    const numberValue = getFormulaNumberValue(sheetId, threshold.value, getters);
    if (numberValue === undefined) {
        return undefined;
    }
    const value = threshold.type === "number"
        ? numberValue
        : minValue + ((maxValue - minValue) * numberValue) / 100;
    return clip(value, minValue, maxValue);
}
function getFormulaNumberValue(sheetId, formula, getters) {
    const value = getters.evaluateFormula(sheetId, formula);
    return isMultipleElementMatrix(value)
        ? undefined
        : tryToNumber(toScalar(value), getters.getLocale());
}
function getInvalidGaugeRuntime(chart, getters) {
    return {
        background: getters.getStyleOfSingleCellChart(chart.background, chart.dataRange).background,
        title: chart.title ?? { text: "" },
        minValue: { value: 0, label: "" },
        maxValue: { value: 100, label: "" },
        gaugeValue: { value: 0, label: CellErrorType.GenericError },
        inflectionValues: [],
        colors: [],
    };
}
function adaptSectionRuleFormulas(sectionRule, adaptCallback) {
    return {
        ...sectionRule,
        rangeMin: adaptCallback(sectionRule.rangeMin),
        rangeMax: adaptCallback(sectionRule.rangeMax),
        lowerInflectionPoint: {
            ...sectionRule.lowerInflectionPoint,
            value: adaptCallback(sectionRule.lowerInflectionPoint.value),
        },
        upperInflectionPoint: {
            ...sectionRule.upperInflectionPoint,
            value: adaptCallback(sectionRule.upperInflectionPoint.value),
        },
    };
}

class GeoChart extends AbstractChart {
    dataSets;
    labelRange;
    background;
    legendPosition;
    type = "geo";
    dataSetsHaveTitle;
    dataSetDesign;
    colorScale;
    missingValueColor;
    region;
    constructor(definition, sheetId, getters) {
        super(definition, sheetId, getters);
        this.dataSets = createDataSets(getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);
        this.labelRange = createValidRange(getters, sheetId, definition.labelRange);
        this.background = definition.background;
        this.legendPosition = definition.legendPosition;
        this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
        this.dataSetDesign = definition.dataSets;
        this.colorScale = definition.colorScale;
        this.missingValueColor = definition.missingValueColor;
        this.region = definition.region;
    }
    static transformDefinition(chartSheetId, definition, applyChange) {
        return transformChartDefinitionWithDataSetsWithZone(chartSheetId, definition, applyChange);
    }
    static validateChartDefinition(validator, definition) {
        return validator.checkValidations(definition, checkDataset, checkLabelRange);
    }
    static getDefinitionFromContextCreation(context) {
        return {
            background: context.background,
            dataSets: context.range ?? [],
            dataSetsHaveTitle: context.dataSetsHaveTitle ?? false,
            legendPosition: context.legendPosition ?? "top",
            title: context.title || { text: "" },
            type: "geo",
            labelRange: context.auxiliaryRange || undefined,
            humanize: context.humanize,
        };
    }
    getContextCreation() {
        const range = [];
        for (const [i, dataSet] of this.dataSets.entries()) {
            range.push({
                ...this.dataSetDesign?.[i],
                dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId),
            });
        }
        return {
            ...this,
            range,
            auxiliaryRange: this.labelRange
                ? this.getters.getRangeString(this.labelRange, this.sheetId)
                : undefined,
        };
    }
    duplicateInDuplicatedSheet(newSheetId) {
        const dataSets = duplicateDataSetsInDuplicatedSheet(this.sheetId, newSheetId, this.dataSets);
        const labelRange = duplicateLabelRangeInDuplicatedSheet(this.sheetId, newSheetId, this.labelRange);
        const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, newSheetId);
        return new GeoChart(definition, newSheetId, this.getters);
    }
    copyInSheetId(sheetId) {
        const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);
        return new GeoChart(definition, sheetId, this.getters);
    }
    getDefinition() {
        return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);
    }
    getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {
        const ranges = [];
        for (const [i, dataSet] of dataSets.entries()) {
            ranges.push({
                ...this.dataSetDesign?.[i],
                dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId),
            });
        }
        return {
            type: "geo",
            dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,
            background: this.background,
            dataSets: ranges,
            legendPosition: this.legendPosition,
            labelRange: labelRange
                ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)
                : undefined,
            title: this.title,
            colorScale: this.colorScale,
            missingValueColor: this.missingValueColor,
            region: this.region,
            humanize: this.humanize,
        };
    }
    getDefinitionForExcel() {
        return 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 GeoChart(definition, this.sheetId, this.getters);
    }
}
function createGeoChartRuntime(chart, getters) {
    const definition = chart.getDefinition();
    const chartData = getGeoChartData(definition, chart.dataSets, chart.labelRange, getters);
    const config = {
        type: "choropleth",
        data: {
            datasets: getGeoChartDatasets(definition, chartData),
        },
        options: {
            ...CHART_COMMON_OPTIONS,
            layout: getChartLayout(definition, chartData),
            scales: getGeoChartScales(definition, chartData),
            plugins: {
                title: getChartTitle(definition, getters),
                tooltip: getGeoChartTooltip(definition, chartData),
                legend: { display: false },
            },
        },
    };
    return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
}

class LineChart extends AbstractChart {
    dataSets;
    labelRange;
    background;
    legendPosition;
    labelsAsText;
    stacked;
    aggregated;
    type = "line";
    dataSetsHaveTitle;
    cumulative;
    dataSetDesign;
    axesDesign;
    fillArea;
    showValues;
    hideDataMarkers;
    zoomable;
    constructor(definition, sheetId, getters) {
        super(definition, sheetId, getters);
        this.dataSets = createDataSets(this.getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);
        this.labelRange = createValidRange(this.getters, sheetId, definition.labelRange);
        this.background = definition.background;
        this.legendPosition = definition.legendPosition;
        this.labelsAsText = definition.labelsAsText;
        this.stacked = definition.stacked;
        this.aggregated = definition.aggregated;
        this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
        this.cumulative = definition.cumulative;
        this.dataSetDesign = definition.dataSets;
        this.axesDesign = definition.axesDesign;
        this.fillArea = definition.fillArea;
        this.showValues = definition.showValues;
        this.hideDataMarkers = definition.hideDataMarkers;
        this.zoomable = definition.zoomable;
    }
    static validateChartDefinition(validator, definition) {
        return validator.checkValidations(definition, checkDataset, checkLabelRange);
    }
    static transformDefinition(chartSheetId, definition, applyChange) {
        return transformChartDefinitionWithDataSetsWithZone(chartSheetId, definition, applyChange);
    }
    static getDefinitionFromContextCreation(context) {
        return {
            background: context.background,
            dataSets: context.range ?? [],
            dataSetsHaveTitle: context.dataSetsHaveTitle ?? false,
            labelsAsText: context.labelsAsText ?? false,
            legendPosition: context.legendPosition ?? "top",
            title: context.title || { text: "" },
            type: "line",
            labelRange: context.auxiliaryRange || undefined,
            stacked: context.stacked ?? false,
            aggregated: context.aggregated ?? false,
            cumulative: context.cumulative ?? false,
            axesDesign: context.axesDesign,
            fillArea: context.fillArea,
            showValues: context.showValues,
            hideDataMarkers: context.hideDataMarkers,
            zoomable: context.zoomable,
            humanize: context.humanize,
        };
    }
    getDefinition() {
        return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);
    }
    getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {
        const ranges = [];
        for (const [i, dataSet] of dataSets.entries()) {
            ranges.push({
                ...this.dataSetDesign?.[i],
                dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId),
            });
        }
        return {
            type: "line",
            dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,
            background: this.background,
            dataSets: ranges,
            legendPosition: this.legendPosition,
            labelRange: labelRange
                ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)
                : undefined,
            title: this.title,
            labelsAsText: this.labelsAsText,
            stacked: this.stacked,
            aggregated: this.aggregated,
            cumulative: this.cumulative,
            axesDesign: this.axesDesign,
            fillArea: this.fillArea,
            showValues: this.showValues,
            hideDataMarkers: this.hideDataMarkers,
            zoomable: this.zoomable,
            humanize: this.humanize,
        };
    }
    getContextCreation() {
        const range = [];
        for (const [i, dataSet] of this.dataSets.entries()) {
            range.push({
                ...this.dataSetDesign?.[i],
                dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId),
            });
        }
        return {
            ...this,
            range,
            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, labelRange } = this.getCommonDataSetAttributesForExcel(this.labelRange, this.dataSets, shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle));
        const definition = this.getDefinition();
        return {
            ...definition,
            backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),
            fontColor: toXlsxHexColor(chartFontColor(this.background)),
            dataSets,
            labelRange,
            verticalAxis: getDefinedAxis(definition),
        };
    }
    duplicateInDuplicatedSheet(newSheetId) {
        const dataSets = duplicateDataSetsInDuplicatedSheet(this.sheetId, newSheetId, this.dataSets);
        const labelRange = duplicateLabelRangeInDuplicatedSheet(this.sheetId, newSheetId, this.labelRange);
        const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, newSheetId);
        return new LineChart(definition, newSheetId, this.getters);
    }
    copyInSheetId(sheetId) {
        const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);
        return new LineChart(definition, sheetId, this.getters);
    }
}
function createLineChartRuntime(chart, getters) {
    const definition = chart.getDefinition();
    const chartData = getLineChartData(definition, chart.dataSets, chart.labelRange, getters);
    const config = {
        type: "line",
        data: {
            labels: chartData.labels,
            datasets: getLineChartDatasets(definition, chartData),
        },
        options: {
            ...CHART_COMMON_OPTIONS,
            layout: getChartLayout(definition, chartData),
            scales: getLineChartScales(definition, chartData),
            plugins: {
                title: getChartTitle(definition, getters),
                legend: getLineChartLegend(definition),
                tooltip: getLineChartTooltip(definition, chartData),
                chartShowValuesPlugin: getChartShowValues(definition, chartData),
            },
        },
    };
    return {
        chartJsConfig: config,
        background: chart.background || BACKGROUND_CHART_COLOR,
    };
}

class PieChart extends AbstractChart {
    dataSets;
    labelRange;
    background;
    legendPosition;
    type = "pie";
    aggregated;
    dataSetsHaveTitle;
    isDoughnut;
    showValues;
    pieHolePercentage;
    constructor(definition, sheetId, getters) {
        super(definition, sheetId, getters);
        this.dataSets = createDataSets(getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);
        this.labelRange = createValidRange(getters, sheetId, definition.labelRange);
        this.background = definition.background;
        this.legendPosition = definition.legendPosition;
        this.aggregated = definition.aggregated;
        this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
        this.isDoughnut = definition.isDoughnut;
        this.showValues = definition.showValues;
        this.pieHolePercentage = definition.pieHolePercentage;
    }
    static transformDefinition(chartSheetId, definition, applyChange) {
        return transformChartDefinitionWithDataSetsWithZone(chartSheetId, definition, applyChange);
    }
    static validateChartDefinition(validator, definition) {
        return validator.checkValidations(definition, checkDataset, checkLabelRange);
    }
    static getDefinitionFromContextCreation(context) {
        return {
            background: context.background,
            dataSets: context.range ?? [],
            dataSetsHaveTitle: context.dataSetsHaveTitle ?? false,
            legendPosition: context.legendPosition ?? "top",
            title: context.title || { text: "" },
            type: "pie",
            labelRange: context.auxiliaryRange || undefined,
            aggregated: context.aggregated ?? false,
            isDoughnut: context.isDoughnut,
            pieHolePercentage: context.pieHolePercentage,
            showValues: context.showValues,
            humanize: context.humanize,
        };
    }
    getDefinition() {
        return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);
    }
    getContextCreation() {
        return {
            ...this,
            range: this.dataSets.map((ds) => ({
                dataRange: 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) => ({
                dataRange: this.getters.getRangeString(ds.dataRange, targetSheetId || this.sheetId),
            })),
            legendPosition: this.legendPosition,
            labelRange: labelRange
                ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)
                : undefined,
            title: this.title,
            aggregated: this.aggregated,
            isDoughnut: this.isDoughnut,
            showValues: this.showValues,
            pieHolePercentage: this.pieHolePercentage,
            humanize: this.humanize,
        };
    }
    duplicateInDuplicatedSheet(newSheetId) {
        const dataSets = duplicateDataSetsInDuplicatedSheet(this.sheetId, newSheetId, this.dataSets);
        const labelRange = duplicateLabelRangeInDuplicatedSheet(this.sheetId, newSheetId, this.labelRange);
        const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, newSheetId);
        return new PieChart(definition, newSheetId, this.getters);
    }
    copyInSheetId(sheetId) {
        const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);
        return new PieChart(definition, sheetId, this.getters);
    }
    getDefinitionForExcel() {
        const { dataSets, labelRange } = this.getCommonDataSetAttributesForExcel(this.labelRange, this.dataSets, shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle));
        return {
            ...this.getDefinition(),
            backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),
            fontColor: toXlsxHexColor(chartFontColor(this.background)),
            dataSets,
            labelRange,
        };
    }
    updateRanges({ applyChange }) {
        const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets(this.getters, applyChange, this.dataSets, this.labelRange);
        if (!isStale) {
            return this;
        }
        const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange);
        return new PieChart(definition, this.sheetId, this.getters);
    }
}
function createPieChartRuntime(chart, getters) {
    const definition = chart.getDefinition();
    const chartData = getPieChartData(definition, chart.dataSets, chart.labelRange, getters);
    const config = {
        type: chart.isDoughnut ? "doughnut" : "pie",
        data: {
            labels: chartData.labels,
            datasets: getPieChartDatasets(definition, chartData),
        },
        options: {
            ...CHART_COMMON_OPTIONS,
            cutout: chart.isDoughnut && definition.pieHolePercentage !== undefined
                ? definition.pieHolePercentage + "%"
                : undefined,
            layout: getChartLayout(definition, chartData),
            plugins: {
                title: getChartTitle(definition, getters),
                legend: getPieChartLegend(definition, chartData),
                tooltip: getPieChartTooltip(definition, chartData),
                chartShowValuesPlugin: getChartShowValues(definition, chartData),
            },
        },
    };
    return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
}

class PyramidChart extends AbstractChart {
    dataSets;
    labelRange;
    background;
    legendPosition;
    aggregated;
    type = "pyramid";
    dataSetsHaveTitle;
    dataSetDesign;
    axesDesign;
    horizontal = true;
    stacked = true;
    showValues;
    constructor(definition, sheetId, getters) {
        super(definition, sheetId, getters);
        this.dataSets = createDataSets(getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);
        this.labelRange = createValidRange(getters, sheetId, definition.labelRange);
        this.background = definition.background;
        this.legendPosition = definition.legendPosition;
        this.aggregated = definition.aggregated;
        this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
        this.dataSetDesign = definition.dataSets;
        this.axesDesign = definition.axesDesign;
        this.showValues = definition.showValues;
    }
    static transformDefinition(chartSheetId, definition, applyChange) {
        return transformChartDefinitionWithDataSetsWithZone(chartSheetId, definition, applyChange);
    }
    static validateChartDefinition(validator, definition) {
        return validator.checkValidations(definition, checkDataset, checkLabelRange);
    }
    static getDefinitionFromContextCreation(context) {
        return {
            background: context.background,
            dataSets: context.range ?? [],
            dataSetsHaveTitle: context.dataSetsHaveTitle ?? false,
            aggregated: context.aggregated ?? false,
            legendPosition: context.legendPosition ?? "top",
            title: context.title || { text: "" },
            type: "pyramid",
            labelRange: context.auxiliaryRange || undefined,
            axesDesign: context.axesDesign,
            horizontal: true,
            stacked: true,
            showValues: context.showValues,
            humanize: context.humanize,
        };
    }
    getContextCreation() {
        const range = [];
        for (const [i, dataSet] of this.dataSets.entries()) {
            range.push({
                ...this.dataSetDesign?.[i],
                dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId),
            });
        }
        return {
            ...this,
            range,
            auxiliaryRange: this.labelRange
                ? this.getters.getRangeString(this.labelRange, this.sheetId)
                : undefined,
        };
    }
    duplicateInDuplicatedSheet(newSheetId) {
        const dataSets = duplicateDataSetsInDuplicatedSheet(this.sheetId, newSheetId, this.dataSets);
        const labelRange = duplicateLabelRangeInDuplicatedSheet(this.sheetId, newSheetId, this.labelRange);
        const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, newSheetId);
        return new PyramidChart(definition, newSheetId, this.getters);
    }
    copyInSheetId(sheetId) {
        const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);
        return new PyramidChart(definition, sheetId, this.getters);
    }
    getDefinition() {
        return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);
    }
    getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {
        const ranges = [];
        for (const [i, dataSet] of dataSets.entries()) {
            ranges.push({
                ...this.dataSetDesign?.[i],
                dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId),
            });
        }
        return {
            type: "pyramid",
            dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,
            background: this.background,
            dataSets: ranges,
            legendPosition: this.legendPosition,
            labelRange: labelRange
                ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)
                : undefined,
            title: this.title,
            aggregated: this.aggregated,
            axesDesign: this.axesDesign,
            horizontal: true,
            stacked: true,
            showValues: this.showValues,
            humanize: this.humanize,
        };
    }
    getDefinitionForExcel(getters) {
        const { dataSets, labelRange } = this.getCommonDataSetAttributesForExcel(this.labelRange, this.dataSets, shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle));
        const definition = this.getDefinition();
        const chartData = getPyramidChartData(definition, this.dataSets, this.labelRange, getters);
        const { dataSetsValues } = chartData;
        const maxValue = Math.max(...dataSetsValues.map((dataSet) => Math.max(...dataSet.data.map(Math.abs))));
        return {
            ...definition,
            horizontal: true,
            backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),
            fontColor: toXlsxHexColor(chartFontColor(this.background)),
            dataSets,
            labelRange,
            verticalAxis: getDefinedAxis(definition),
            maxValue,
        };
    }
    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 PyramidChart(definition, this.sheetId, this.getters);
    }
}
function createPyramidChartRuntime(chart, getters) {
    const definition = chart.getDefinition();
    const chartData = getPyramidChartData(definition, chart.dataSets, chart.labelRange, getters);
    const config = {
        type: "bar",
        data: {
            labels: chartData.labels,
            datasets: getBarChartDatasets(definition, chartData),
        },
        options: {
            ...CHART_COMMON_OPTIONS,
            indexAxis: "y",
            layout: getChartLayout(definition, chartData),
            scales: getPyramidChartScales(definition, chartData),
            plugins: {
                title: getChartTitle(definition, getters),
                legend: getBarChartLegend(definition),
                tooltip: getPyramidChartTooltip(definition, chartData),
                chartShowValuesPlugin: getPyramidChartShowValues(definition, chartData),
            },
        },
    };
    return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
}

class RadarChart extends AbstractChart {
    dataSets;
    labelRange;
    background;
    legendPosition;
    stacked;
    aggregated;
    type = "radar";
    dataSetsHaveTitle;
    dataSetDesign;
    fillArea;
    showValues;
    hideDataMarkers;
    constructor(definition, sheetId, getters) {
        super(definition, sheetId, getters);
        this.dataSets = createDataSets(getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);
        this.labelRange = createValidRange(getters, sheetId, definition.labelRange);
        this.background = definition.background;
        this.legendPosition = definition.legendPosition;
        this.stacked = definition.stacked;
        this.aggregated = definition.aggregated;
        this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
        this.dataSetDesign = definition.dataSets;
        this.fillArea = definition.fillArea;
        this.showValues = definition.showValues;
        this.hideDataMarkers = definition.hideDataMarkers;
    }
    static transformDefinition(chartSheetId, definition, applyChange) {
        return transformChartDefinitionWithDataSetsWithZone(chartSheetId, definition, applyChange);
    }
    static validateChartDefinition(validator, definition) {
        return validator.checkValidations(definition, checkDataset, checkLabelRange);
    }
    static getDefinitionFromContextCreation(context) {
        return {
            background: context.background,
            dataSets: context.range ?? [],
            dataSetsHaveTitle: context.dataSetsHaveTitle ?? false,
            stacked: context.stacked ?? false,
            aggregated: context.aggregated ?? false,
            legendPosition: context.legendPosition ?? "top",
            title: context.title || { text: "" },
            type: "radar",
            labelRange: context.auxiliaryRange || undefined,
            fillArea: context.fillArea ?? false,
            showValues: context.showValues ?? false,
            hideDataMarkers: context.hideDataMarkers,
            humanize: context.humanize,
        };
    }
    getContextCreation() {
        const range = [];
        for (const [i, dataSet] of this.dataSets.entries()) {
            range.push({
                ...this.dataSetDesign?.[i],
                dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId),
            });
        }
        return {
            ...this,
            range,
            auxiliaryRange: this.labelRange
                ? this.getters.getRangeString(this.labelRange, this.sheetId)
                : undefined,
        };
    }
    duplicateInDuplicatedSheet(newSheetId) {
        const dataSets = duplicateDataSetsInDuplicatedSheet(this.sheetId, newSheetId, this.dataSets);
        const labelRange = duplicateLabelRangeInDuplicatedSheet(this.sheetId, newSheetId, this.labelRange);
        const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, newSheetId);
        return new RadarChart(definition, newSheetId, this.getters);
    }
    copyInSheetId(sheetId) {
        const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);
        return new RadarChart(definition, sheetId, this.getters);
    }
    getDefinition() {
        return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);
    }
    getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {
        const ranges = [];
        for (const [i, dataSet] of dataSets.entries()) {
            ranges.push({
                ...this.dataSetDesign?.[i],
                dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId),
            });
        }
        return {
            type: "radar",
            dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,
            background: this.background,
            dataSets: ranges,
            legendPosition: this.legendPosition,
            labelRange: labelRange
                ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)
                : undefined,
            title: this.title,
            stacked: this.stacked,
            aggregated: this.aggregated,
            fillArea: this.fillArea,
            showValues: this.showValues,
            hideDataMarkers: this.hideDataMarkers,
            humanize: this.humanize,
        };
    }
    getDefinitionForExcel() {
        const { dataSets, labelRange } = this.getCommonDataSetAttributesForExcel(this.labelRange, this.dataSets, shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle));
        const definition = this.getDefinition();
        return {
            ...definition,
            backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),
            fontColor: toXlsxHexColor(chartFontColor(this.background)),
            dataSets,
            labelRange,
        };
    }
    updateRanges({ applyChange }) {
        const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets(this.getters, applyChange, this.dataSets, this.labelRange);
        if (!isStale) {
            return this;
        }
        const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange);
        return new RadarChart(definition, this.sheetId, this.getters);
    }
}
function createRadarChartRuntime(chart, getters) {
    const definition = chart.getDefinition();
    const chartData = getRadarChartData(definition, chart.dataSets, chart.labelRange, getters);
    const config = {
        type: "radar",
        data: {
            labels: chartData.labels,
            datasets: getRadarChartDatasets(definition, chartData),
        },
        options: {
            ...CHART_COMMON_OPTIONS,
            layout: getChartLayout(definition, chartData),
            scales: getRadarChartScales(definition, chartData),
            plugins: {
                title: getChartTitle(definition, getters),
                legend: getRadarChartLegend(definition),
                tooltip: getRadarChartTooltip(definition, chartData),
                chartShowValuesPlugin: getChartShowValues(definition, chartData),
            },
        },
    };
    return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
}

class ScatterChart extends AbstractChart {
    dataSets;
    labelRange;
    background;
    legendPosition;
    labelsAsText;
    aggregated;
    type = "scatter";
    dataSetsHaveTitle;
    dataSetDesign;
    axesDesign;
    showValues;
    constructor(definition, sheetId, getters) {
        super(definition, sheetId, getters);
        this.dataSets = createDataSets(this.getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);
        this.labelRange = createValidRange(this.getters, sheetId, definition.labelRange);
        this.background = definition.background;
        this.legendPosition = definition.legendPosition;
        this.labelsAsText = definition.labelsAsText;
        this.aggregated = definition.aggregated;
        this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
        this.dataSetDesign = definition.dataSets;
        this.axesDesign = definition.axesDesign;
        this.showValues = definition.showValues;
    }
    static validateChartDefinition(validator, definition) {
        return validator.checkValidations(definition, checkDataset, checkLabelRange);
    }
    static transformDefinition(chartSheetId, definition, applyChange) {
        return transformChartDefinitionWithDataSetsWithZone(chartSheetId, definition, applyChange);
    }
    static getDefinitionFromContextCreation(context) {
        return {
            background: context.background,
            dataSets: context.range ?? [],
            dataSetsHaveTitle: context.dataSetsHaveTitle ?? false,
            labelsAsText: context.labelsAsText ?? false,
            legendPosition: context.legendPosition ?? "top",
            title: context.title || { text: "" },
            type: "scatter",
            labelRange: context.auxiliaryRange || undefined,
            aggregated: context.aggregated ?? false,
            axesDesign: context.axesDesign,
            showValues: context.showValues,
            humanize: context.humanize,
        };
    }
    getDefinition() {
        return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);
    }
    getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {
        const ranges = [];
        for (const [i, dataSet] of dataSets.entries()) {
            ranges.push({
                ...this.dataSetDesign?.[i],
                dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId),
            });
        }
        return {
            type: "scatter",
            dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,
            background: this.background,
            dataSets: ranges,
            legendPosition: this.legendPosition,
            labelRange: labelRange
                ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)
                : undefined,
            title: this.title,
            labelsAsText: this.labelsAsText,
            aggregated: this.aggregated,
            axesDesign: this.axesDesign,
            showValues: this.showValues,
            humanize: this.humanize,
        };
    }
    getContextCreation() {
        const range = [];
        for (const [i, dataSet] of this.dataSets.entries()) {
            range.push({
                ...this.dataSetDesign?.[i],
                dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId),
            });
        }
        return {
            ...this,
            range,
            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 ScatterChart(definition, this.sheetId, this.getters);
    }
    getDefinitionForExcel() {
        const dataSets = this.dataSets
            .map((ds) => toExcelDataset(this.getters, ds))
            .filter((ds) => ds.range !== "");
        const labelRange = toExcelLabelRange(this.getters, this.labelRange, shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle));
        const definition = this.getDefinition();
        return {
            ...definition,
            backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),
            fontColor: toXlsxHexColor(chartFontColor(this.background)),
            dataSets,
            labelRange,
            verticalAxis: getDefinedAxis(definition),
        };
    }
    duplicateInDuplicatedSheet(newSheetId) {
        const dataSets = duplicateDataSetsInDuplicatedSheet(this.sheetId, newSheetId, this.dataSets);
        const labelRange = duplicateLabelRangeInDuplicatedSheet(this.sheetId, newSheetId, this.labelRange);
        const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, newSheetId);
        return new ScatterChart(definition, newSheetId, this.getters);
    }
    copyInSheetId(sheetId) {
        const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);
        return new ScatterChart(definition, sheetId, this.getters);
    }
}
function createScatterChartRuntime(chart, getters) {
    const definition = chart.getDefinition();
    const chartData = getLineChartData(definition, chart.dataSets, chart.labelRange, getters);
    const config = {
        // use chartJS line chart and disable the lines instead of chartJS scatter chart. This is because the scatter chart
        // have less options than the line chart (it only works with linear labels)
        type: "line",
        data: {
            labels: chartData.labels,
            datasets: getScatterChartDatasets(definition, chartData),
        },
        options: {
            ...CHART_COMMON_OPTIONS,
            layout: getChartLayout(definition, chartData),
            scales: getScatterChartScales(definition, chartData),
            plugins: {
                title: getChartTitle(definition, getters),
                legend: getScatterChartLegend(definition),
                tooltip: getLineChartTooltip(definition, chartData),
                chartShowValuesPlugin: getChartShowValues(definition, chartData),
            },
        },
    };
    return {
        chartJsConfig: config,
        background: chart.background || BACKGROUND_CHART_COLOR,
    };
}

class SunburstChart extends AbstractChart {
    dataSets;
    labelRange;
    background;
    legendPosition;
    type = "sunburst";
    dataSetsHaveTitle;
    showValues;
    showLabels;
    valuesDesign;
    groupColors;
    pieHolePercentage;
    constructor(definition, sheetId, getters) {
        super(definition, sheetId, getters);
        this.dataSets = createDataSets(getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);
        this.labelRange = createValidRange(getters, sheetId, definition.labelRange);
        this.background = definition.background;
        this.legendPosition = definition.legendPosition;
        this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
        this.showValues = definition.showValues;
        this.showLabels = definition.showLabels;
        this.valuesDesign = definition.valuesDesign;
        this.groupColors = definition.groupColors;
        this.pieHolePercentage = definition.pieHolePercentage;
    }
    static transformDefinition(chartSheetId, definition, applyChange) {
        return transformChartDefinitionWithDataSetsWithZone(chartSheetId, definition, applyChange);
    }
    static validateChartDefinition(validator, definition) {
        return validator.checkValidations(definition, checkDataset, checkLabelRange);
    }
    static getDefinitionFromContextCreation(context) {
        const dataSets = [];
        if (context.hierarchicalRanges?.length) {
            dataSets.push(...context.hierarchicalRanges);
        }
        else if (context.auxiliaryRange) {
            dataSets.push({ ...context.range?.[0], dataRange: context.auxiliaryRange });
        }
        return {
            background: context.background,
            dataSets,
            dataSetsHaveTitle: context.dataSetsHaveTitle ?? false,
            legendPosition: context.legendPosition ?? "top",
            title: context.title || { text: "" },
            type: "sunburst",
            labelRange: context.range?.[0]?.dataRange,
            showValues: context.showValues,
            showLabels: context.showLabels,
            valuesDesign: context.valuesDesign,
            groupColors: context.groupColors,
            humanize: context.humanize,
        };
    }
    getDefinition() {
        return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);
    }
    getContextCreation() {
        const leafRange = this.dataSets.at(-1)?.dataRange;
        return {
            ...this,
            range: this.labelRange
                ? [{ dataRange: this.getters.getRangeString(this.labelRange, this.sheetId) }]
                : [],
            auxiliaryRange: leafRange ? this.getters.getRangeString(leafRange, this.sheetId) : undefined,
            hierarchicalRanges: this.dataSets.map((ds) => ({
                dataRange: this.getters.getRangeString(ds.dataRange, this.sheetId),
            })),
        };
    }
    getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {
        return {
            type: "sunburst",
            dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,
            background: this.background,
            dataSets: dataSets.map((ds) => ({
                dataRange: this.getters.getRangeString(ds.dataRange, targetSheetId || this.sheetId),
            })),
            legendPosition: this.legendPosition,
            labelRange: labelRange
                ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)
                : undefined,
            title: this.title,
            showValues: this.showValues,
            showLabels: this.showLabels,
            valuesDesign: this.valuesDesign,
            groupColors: this.groupColors,
            pieHolePercentage: this.pieHolePercentage,
            humanize: this.humanize,
        };
    }
    duplicateInDuplicatedSheet(newSheetId) {
        const dataSets = duplicateDataSetsInDuplicatedSheet(this.sheetId, newSheetId, this.dataSets);
        const labelRange = duplicateLabelRangeInDuplicatedSheet(this.sheetId, newSheetId, this.labelRange);
        const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, newSheetId);
        return new SunburstChart(definition, newSheetId, this.getters);
    }
    copyInSheetId(sheetId) {
        const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);
        return new SunburstChart(definition, sheetId, this.getters);
    }
    getDefinitionForExcel() {
        return 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 SunburstChart(definition, this.sheetId, this.getters);
    }
}
function createSunburstChartRuntime(chart, getters) {
    const definition = chart.getDefinition();
    const chartData = getHierarchalChartData(definition, chart.dataSets, chart.labelRange, getters);
    const config = {
        type: "doughnut",
        data: {
            datasets: getSunburstChartDatasets(definition, chartData),
        },
        options: {
            cutout: chart.pieHolePercentage === undefined ? "25%" : `${chart.pieHolePercentage}%`,
            ...CHART_COMMON_OPTIONS,
            layout: getChartLayout(definition, chartData),
            plugins: {
                title: getChartTitle(definition, getters),
                legend: getSunburstChartLegend(definition),
                tooltip: getSunburstChartTooltip(definition, chartData),
                sunburstLabelsPlugin: getSunburstShowValues(definition, chartData),
                sunburstHoverPlugin: { enabled: true },
            },
        },
    };
    return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
}

class TreeMapChart extends AbstractChart {
    static defaults = {
        background: BACKGROUND_CHART_COLOR,
        legendPosition: "top",
        dataSetsHaveTitle: false,
        showHeaders: true,
        headersColor: "#000000",
    };
    dataSets;
    labelRange;
    background;
    legendPosition;
    type = "treemap";
    dataSetsHaveTitle;
    showHeaders;
    headerDesign;
    showValues;
    showLabels;
    valuesDesign;
    coloringOptions;
    constructor(definition, sheetId, getters) {
        super(definition, sheetId, getters);
        this.dataSets = createDataSets(getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);
        this.labelRange = createValidRange(getters, sheetId, definition.labelRange);
        this.background = definition.background;
        this.legendPosition = definition.legendPosition;
        this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
        this.showHeaders = definition.showHeaders;
        this.headerDesign = definition.headerDesign;
        this.showValues = definition.showValues;
        this.showLabels = definition.showLabels;
        this.valuesDesign = definition.valuesDesign;
        this.coloringOptions = definition.coloringOptions;
    }
    static transformDefinition(chartSheetId, definition, applyChange) {
        return transformChartDefinitionWithDataSetsWithZone(chartSheetId, definition, applyChange);
    }
    static validateChartDefinition(validator, definition) {
        return validator.checkValidations(definition, checkDataset, checkLabelRange);
    }
    static getDefinitionFromContextCreation(context) {
        const dataSets = [];
        if (context.hierarchicalRanges?.length) {
            dataSets.push(...context.hierarchicalRanges);
        }
        else if (context.auxiliaryRange) {
            dataSets.push({ ...context.range?.[0], dataRange: context.auxiliaryRange });
        }
        return {
            background: context.background,
            dataSets,
            dataSetsHaveTitle: context.dataSetsHaveTitle ?? false,
            legendPosition: context.legendPosition ?? "top",
            title: context.title || { text: "" },
            type: "treemap",
            labelRange: context.range?.[0]?.dataRange,
            showValues: context.showValues,
            showHeaders: context.showHeaders,
            headerDesign: context.headerDesign,
            showLabels: context.showLabels,
            valuesDesign: context.valuesDesign,
            coloringOptions: context.treemapColoringOptions,
            humanize: context.humanize,
        };
    }
    getContextCreation() {
        const leafRange = this.dataSets.at(-1)?.dataRange;
        return {
            ...this,
            range: this.labelRange
                ? [{ dataRange: this.getters.getRangeString(this.labelRange, this.sheetId) }]
                : [],
            auxiliaryRange: leafRange ? this.getters.getRangeString(leafRange, this.sheetId) : undefined,
            hierarchicalRanges: this.dataSets.map((ds) => ({
                dataRange: this.getters.getRangeString(ds.dataRange, this.sheetId),
            })),
        };
    }
    duplicateInDuplicatedSheet(newSheetId) {
        const dataSets = duplicateDataSetsInDuplicatedSheet(this.sheetId, newSheetId, this.dataSets);
        const labelRange = duplicateLabelRangeInDuplicatedSheet(this.sheetId, newSheetId, this.labelRange);
        const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, newSheetId);
        return new TreeMapChart(definition, newSheetId, this.getters);
    }
    copyInSheetId(sheetId) {
        const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);
        return new TreeMapChart(definition, sheetId, this.getters);
    }
    getDefinition() {
        return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);
    }
    getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {
        const ranges = dataSets.map((dataSet) => ({
            dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId),
        }));
        return {
            type: "treemap",
            dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,
            background: this.background,
            dataSets: ranges,
            legendPosition: this.legendPosition,
            labelRange: labelRange
                ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)
                : undefined,
            title: this.title,
            showValues: this.showValues,
            showHeaders: this.showHeaders,
            headerDesign: this.headerDesign,
            showLabels: this.showLabels,
            valuesDesign: this.valuesDesign,
            coloringOptions: this.coloringOptions,
            humanize: this.humanize,
        };
    }
    getDefinitionForExcel() {
        return 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 TreeMapChart(definition, this.sheetId, this.getters);
    }
}
function createTreeMapChartRuntime(chart, getters) {
    const definition = chart.getDefinition();
    const chartData = getHierarchalChartData(definition, chart.dataSets, chart.labelRange, getters);
    const config = {
        type: "treemap",
        data: {
            labels: chartData.labels,
            datasets: getTreeMapChartDatasets(definition, chartData),
        },
        options: {
            ...CHART_COMMON_OPTIONS,
            layout: getChartLayout(definition, chartData),
            plugins: {
                title: getChartTitle(definition, getters),
                legend: { display: false },
                tooltip: getTreeMapChartTooltip(definition, chartData),
            },
        },
    };
    return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
}

class WaterfallChart extends AbstractChart {
    dataSets;
    labelRange;
    background;
    verticalAxisPosition;
    legendPosition;
    aggregated;
    type = "waterfall";
    dataSetsHaveTitle;
    showSubTotals;
    firstValueAsSubtotal;
    showConnectorLines;
    positiveValuesColor;
    negativeValuesColor;
    subTotalValuesColor;
    dataSetDesign;
    axesDesign;
    showValues;
    zoomable;
    constructor(definition, sheetId, getters) {
        super(definition, sheetId, getters);
        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.aggregated = definition.aggregated;
        this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
        this.showSubTotals = definition.showSubTotals;
        this.showConnectorLines = definition.showConnectorLines;
        this.positiveValuesColor = definition.positiveValuesColor;
        this.negativeValuesColor = definition.negativeValuesColor;
        this.subTotalValuesColor = definition.subTotalValuesColor;
        this.firstValueAsSubtotal = definition.firstValueAsSubtotal;
        this.dataSetDesign = definition.dataSets;
        this.axesDesign = definition.axesDesign;
        this.showValues = definition.showValues;
        this.zoomable = definition.zoomable;
    }
    static transformDefinition(chartSheetId, definition, applyChange) {
        return transformChartDefinitionWithDataSetsWithZone(chartSheetId, definition, applyChange);
    }
    static validateChartDefinition(validator, definition) {
        return validator.checkValidations(definition, checkDataset, checkLabelRange);
    }
    static getDefinitionFromContextCreation(context) {
        return {
            background: context.background,
            dataSets: context.range ? context.range : [],
            dataSetsHaveTitle: context.dataSetsHaveTitle ?? false,
            aggregated: context.aggregated ?? false,
            legendPosition: context.legendPosition ?? "top",
            title: context.title || { text: "" },
            type: "waterfall",
            verticalAxisPosition: "left",
            labelRange: context.auxiliaryRange || undefined,
            showSubTotals: context.showSubTotals ?? false,
            showConnectorLines: context.showConnectorLines ?? true,
            firstValueAsSubtotal: context.firstValueAsSubtotal ?? false,
            axesDesign: context.axesDesign,
            showValues: context.showValues,
            zoomable: context.zoomable ?? false,
            humanize: context.humanize,
        };
    }
    getContextCreation() {
        const range = [];
        for (const [i, dataSet] of this.dataSets.entries()) {
            range.push({
                ...this.dataSetDesign?.[i],
                dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId),
            });
        }
        return {
            ...this,
            range,
            auxiliaryRange: this.labelRange
                ? this.getters.getRangeString(this.labelRange, this.sheetId)
                : undefined,
        };
    }
    duplicateInDuplicatedSheet(newSheetId) {
        const dataSets = duplicateDataSetsInDuplicatedSheet(this.sheetId, newSheetId, this.dataSets);
        const labelRange = duplicateLabelRangeInDuplicatedSheet(this.sheetId, newSheetId, this.labelRange);
        const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, newSheetId);
        return new WaterfallChart(definition, newSheetId, this.getters);
    }
    copyInSheetId(sheetId) {
        const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);
        return new WaterfallChart(definition, sheetId, this.getters);
    }
    getDefinition() {
        return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);
    }
    getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {
        const ranges = [];
        for (const [i, dataSet] of dataSets.entries()) {
            ranges.push({
                ...this.dataSetDesign?.[i],
                dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId),
            });
        }
        return {
            type: "waterfall",
            dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,
            background: this.background,
            dataSets: ranges,
            legendPosition: this.legendPosition,
            verticalAxisPosition: this.verticalAxisPosition,
            labelRange: labelRange
                ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)
                : undefined,
            title: this.title,
            aggregated: this.aggregated,
            showSubTotals: this.showSubTotals,
            showConnectorLines: this.showConnectorLines,
            positiveValuesColor: this.positiveValuesColor,
            negativeValuesColor: this.negativeValuesColor,
            subTotalValuesColor: this.subTotalValuesColor,
            firstValueAsSubtotal: this.firstValueAsSubtotal,
            axesDesign: this.axesDesign,
            showValues: this.showValues,
            zoomable: this.zoomable,
            humanize: this.humanize,
        };
    }
    getDefinitionForExcel() {
        // TODO: implement export excel
        return 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 WaterfallChart(definition, this.sheetId, this.getters);
    }
}
function createWaterfallChartRuntime(chart, getters) {
    const definition = chart.getDefinition();
    const chartData = getBarChartData(definition, chart.dataSets, chart.labelRange, getters);
    const { labels, datasets } = getWaterfallDatasetAndLabels(definition, chartData);
    const config = {
        type: "bar",
        data: {
            labels,
            datasets,
        },
        options: {
            ...CHART_COMMON_OPTIONS,
            layout: getChartLayout(definition, chartData),
            scales: getWaterfallChartScales(definition, chartData),
            plugins: {
                title: getChartTitle(definition, getters),
                legend: getWaterfallChartLegend(definition),
                tooltip: getWaterfallChartTooltip(definition, chartData),
                chartShowValuesPlugin: getWaterfallChartShowValues(definition, chartData),
                waterfallLinesPlugin: { showConnectorLines: definition.showConnectorLines },
            },
        },
    };
    return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
}

/**
 * This registry is intended to map a cell content (raw string) to
 * an instance of a cell.
 */
const chartRegistry = new Registry();
chartRegistry.add("bar", {
    match: (type) => type === "bar",
    createChart: (definition, sheetId, getters) => new BarChart(definition, sheetId, getters),
    getChartRuntime: createBarChartRuntime,
    validateChartDefinition: BarChart.validateChartDefinition,
    transformDefinition: BarChart.transformDefinition,
    getChartDefinitionFromContextCreation: BarChart.getDefinitionFromContextCreation,
    sequence: 10,
});
chartRegistry.add("combo", {
    match: (type) => type === "combo",
    createChart: (definition, sheetId, getters) => new ComboChart(definition, sheetId, getters),
    getChartRuntime: createComboChartRuntime,
    validateChartDefinition: ComboChart.validateChartDefinition,
    transformDefinition: ComboChart.transformDefinition,
    getChartDefinitionFromContextCreation: ComboChart.getDefinitionFromContextCreation,
    sequence: 15,
});
chartRegistry.add("line", {
    match: (type) => type === "line",
    createChart: (definition, sheetId, getters) => new LineChart(definition, sheetId, getters),
    getChartRuntime: createLineChartRuntime,
    validateChartDefinition: LineChart.validateChartDefinition,
    transformDefinition: LineChart.transformDefinition,
    getChartDefinitionFromContextCreation: LineChart.getDefinitionFromContextCreation,
    sequence: 20,
});
chartRegistry.add("pie", {
    match: (type) => type === "pie",
    createChart: (definition, sheetId, getters) => new PieChart(definition, sheetId, getters),
    getChartRuntime: createPieChartRuntime,
    validateChartDefinition: PieChart.validateChartDefinition,
    transformDefinition: PieChart.transformDefinition,
    getChartDefinitionFromContextCreation: PieChart.getDefinitionFromContextCreation,
    sequence: 30,
});
chartRegistry.add("scorecard", {
    match: (type) => type === "scorecard",
    createChart: (definition, sheetId, getters) => new ScorecardChart$1(definition, sheetId, getters),
    getChartRuntime: createScorecardChartRuntime,
    validateChartDefinition: ScorecardChart$1.validateChartDefinition,
    transformDefinition: ScorecardChart$1.transformDefinition,
    getChartDefinitionFromContextCreation: ScorecardChart$1.getDefinitionFromContextCreation,
    sequence: 40,
});
chartRegistry.add("gauge", {
    match: (type) => type === "gauge",
    createChart: (definition, sheetId, getters) => new GaugeChart(definition, sheetId, getters),
    getChartRuntime: createGaugeChartRuntime,
    validateChartDefinition: GaugeChart.validateChartDefinition,
    transformDefinition: GaugeChart.transformDefinition,
    getChartDefinitionFromContextCreation: GaugeChart.getDefinitionFromContextCreation,
    sequence: 50,
});
chartRegistry.add("scatter", {
    match: (type) => type === "scatter",
    createChart: (definition, sheetId, getters) => new ScatterChart(definition, sheetId, getters),
    getChartRuntime: createScatterChartRuntime,
    validateChartDefinition: ScatterChart.validateChartDefinition,
    transformDefinition: ScatterChart.transformDefinition,
    getChartDefinitionFromContextCreation: ScatterChart.getDefinitionFromContextCreation,
    sequence: 60,
});
chartRegistry.add("waterfall", {
    match: (type) => type === "waterfall",
    createChart: (definition, sheetId, getters) => new WaterfallChart(definition, sheetId, getters),
    getChartRuntime: createWaterfallChartRuntime,
    validateChartDefinition: WaterfallChart.validateChartDefinition,
    transformDefinition: WaterfallChart.transformDefinition,
    getChartDefinitionFromContextCreation: WaterfallChart.getDefinitionFromContextCreation,
    sequence: 70,
});
chartRegistry.add("pyramid", {
    match: (type) => type === "pyramid",
    createChart: (definition, sheetId, getters) => new PyramidChart(definition, sheetId, getters),
    getChartRuntime: createPyramidChartRuntime,
    validateChartDefinition: PyramidChart.validateChartDefinition,
    transformDefinition: PyramidChart.transformDefinition,
    getChartDefinitionFromContextCreation: PyramidChart.getDefinitionFromContextCreation,
    sequence: 80,
    dataSeriesLimit: 2,
});
chartRegistry.add("radar", {
    match: (type) => type === "radar",
    createChart: (definition, sheetId, getters) => new RadarChart(definition, sheetId, getters),
    getChartRuntime: createRadarChartRuntime,
    validateChartDefinition: RadarChart.validateChartDefinition,
    transformDefinition: RadarChart.transformDefinition,
    getChartDefinitionFromContextCreation: RadarChart.getDefinitionFromContextCreation,
    sequence: 80,
});
chartRegistry.add("geo", {
    match: (type) => type === "geo",
    createChart: (definition, sheetId, getters) => new GeoChart(definition, sheetId, getters),
    getChartRuntime: createGeoChartRuntime,
    validateChartDefinition: GeoChart.validateChartDefinition,
    transformDefinition: GeoChart.transformDefinition,
    getChartDefinitionFromContextCreation: GeoChart.getDefinitionFromContextCreation,
    sequence: 90,
    dataSeriesLimit: 1,
});
chartRegistry.add("funnel", {
    match: (type) => type === "funnel",
    createChart: (definition, sheetId, getters) => new FunnelChart(definition, sheetId, getters),
    getChartRuntime: createFunnelChartRuntime,
    validateChartDefinition: FunnelChart.validateChartDefinition,
    transformDefinition: FunnelChart.transformDefinition,
    getChartDefinitionFromContextCreation: FunnelChart.getDefinitionFromContextCreation,
    sequence: 100,
    dataSeriesLimit: 1,
});
chartRegistry.add("sunburst", {
    match: (type) => type === "sunburst",
    createChart: (definition, sheetId, getters) => new SunburstChart(definition, sheetId, getters),
    getChartRuntime: createSunburstChartRuntime,
    validateChartDefinition: SunburstChart.validateChartDefinition,
    transformDefinition: SunburstChart.transformDefinition,
    getChartDefinitionFromContextCreation: SunburstChart.getDefinitionFromContextCreation,
    sequence: 30,
});
chartRegistry.add("treemap", {
    match: (type) => type === "treemap",
    createChart: (definition, sheetId, getters) => new TreeMapChart(definition, sheetId, getters),
    getChartRuntime: createTreeMapChartRuntime,
    validateChartDefinition: TreeMapChart.validateChartDefinition,
    transformDefinition: TreeMapChart.transformDefinition,
    getChartDefinitionFromContextCreation: TreeMapChart.getDefinitionFromContextCreation,
    sequence: 100,
});
const chartComponentRegistry = new Registry();
chartComponentRegistry.add("line", ZoomableChartJsComponent);
chartComponentRegistry.add("bar", ZoomableChartJsComponent);
chartComponentRegistry.add("combo", ZoomableChartJsComponent);
chartComponentRegistry.add("pie", ChartJsComponent);
chartComponentRegistry.add("gauge", GaugeChartComponent);
chartComponentRegistry.add("scatter", ChartJsComponent);
chartComponentRegistry.add("scorecard", ScorecardChart);
chartComponentRegistry.add("waterfall", ZoomableChartJsComponent);
chartComponentRegistry.add("pyramid", ChartJsComponent);
chartComponentRegistry.add("radar", ChartJsComponent);
chartComponentRegistry.add("geo", ChartJsComponent);
chartComponentRegistry.add("funnel", ChartJsComponent);
chartComponentRegistry.add("sunburst", ChartJsComponent);
chartComponentRegistry.add("treemap", ChartJsComponent);
const chartCategories = {
    line: _t("Line"),
    column: _t("Column"),
    bar: _t("Bar"),
    area: _t("Area"),
    pie: _t("Pie"),
    hierarchical: _t("Hierarchical"),
    misc: _t("Miscellaneous"),
};
const chartSubtypeRegistry = new Registry();
chartSubtypeRegistry
    .add("line", {
    matcher: (definition) => definition.type === "line" && !definition.stacked && !definition.fillArea,
    displayName: _t("Line"),
    chartType: "line",
    chartSubtype: "line",
    subtypeDefinition: { stacked: false, fillArea: false },
    category: "line",
    preview: "o-spreadsheet-ChartPreview.LINE_CHART",
})
    .add("stacked_line", {
    matcher: (definition) => definition.type === "line" && !definition.fillArea && !!definition.stacked,
    displayName: _t("Stacked Line"),
    chartType: "line",
    chartSubtype: "stacked_line",
    subtypeDefinition: { stacked: true, fillArea: false },
    category: "line",
    preview: "o-spreadsheet-ChartPreview.STACKED_LINE_CHART",
})
    .add("area", {
    matcher: (definition) => definition.type === "line" && !definition.stacked && !!definition.fillArea,
    displayName: _t("Area"),
    chartType: "line",
    chartSubtype: "area",
    subtypeDefinition: { stacked: false, fillArea: true },
    category: "area",
    preview: "o-spreadsheet-ChartPreview.AREA_CHART",
})
    .add("stacked_area", {
    matcher: (definition) => definition.type === "line" && definition.stacked && !!definition.fillArea,
    displayName: _t("Stacked Area"),
    chartType: "line",
    chartSubtype: "stacked_area",
    subtypeDefinition: { stacked: true, fillArea: true },
    category: "area",
    preview: "o-spreadsheet-ChartPreview.STACKED_AREA_CHART",
})
    .add("scatter", {
    displayName: _t("Scatter"),
    chartType: "scatter",
    chartSubtype: "scatter",
    category: "misc",
    preview: "o-spreadsheet-ChartPreview.SCATTER_CHART",
})
    .add("column", {
    matcher: (definition) => definition.type === "bar" && !definition.stacked && !definition.horizontal,
    displayName: _t("Column"),
    chartType: "bar",
    chartSubtype: "column",
    subtypeDefinition: { stacked: false, horizontal: false },
    category: "column",
    preview: "o-spreadsheet-ChartPreview.COLUMN_CHART",
})
    .add("stacked_column", {
    matcher: (definition) => definition.type === "bar" && definition.stacked && !definition.horizontal,
    displayName: _t("Stacked Column"),
    chartType: "bar",
    chartSubtype: "stacked_column",
    subtypeDefinition: { stacked: true, horizontal: false },
    category: "column",
    preview: "o-spreadsheet-ChartPreview.STACKED_COLUMN_CHART",
})
    .add("bar", {
    matcher: (definition) => definition.type === "bar" && !definition.stacked && !!definition.horizontal,
    displayName: _t("Bar"),
    chartType: "bar",
    chartSubtype: "bar",
    subtypeDefinition: { horizontal: true, stacked: false },
    category: "bar",
    preview: "o-spreadsheet-ChartPreview.BAR_CHART",
})
    .add("stacked_bar", {
    matcher: (definition) => definition.type === "bar" && definition.stacked && !!definition.horizontal,
    displayName: _t("Stacked Bar"),
    chartType: "bar",
    chartSubtype: "stacked_bar",
    subtypeDefinition: { horizontal: true, stacked: true },
    category: "bar",
    preview: "o-spreadsheet-ChartPreview.STACKED_BAR_CHART",
})
    .add("combo", {
    displayName: _t("Combo"),
    chartSubtype: "combo",
    chartType: "combo",
    category: "line",
    preview: "o-spreadsheet-ChartPreview.COMBO_CHART",
})
    .add("pie", {
    matcher: (definition) => definition.type === "pie" && !definition.isDoughnut,
    displayName: _t("Pie"),
    chartSubtype: "pie",
    chartType: "pie",
    subtypeDefinition: { isDoughnut: false },
    category: "pie",
    preview: "o-spreadsheet-ChartPreview.PIE_CHART",
})
    .add("doughnut", {
    matcher: (definition) => definition.type === "pie" && !!definition.isDoughnut,
    displayName: _t("Doughnut"),
    chartSubtype: "doughnut",
    chartType: "pie",
    subtypeDefinition: { isDoughnut: true },
    category: "pie",
    preview: "o-spreadsheet-ChartPreview.DOUGHNUT_CHART",
})
    .add("gauge", {
    displayName: _t("Gauge"),
    chartSubtype: "gauge",
    chartType: "gauge",
    category: "misc",
    preview: "o-spreadsheet-ChartPreview.GAUGE_CHART",
})
    .add("scorecard", {
    displayName: _t("Scorecard"),
    chartSubtype: "scorecard",
    chartType: "scorecard",
    category: "misc",
    preview: "o-spreadsheet-ChartPreview.SCORECARD_CHART",
})
    .add("waterfall", {
    displayName: _t("Waterfall"),
    chartSubtype: "waterfall",
    chartType: "waterfall",
    category: "misc",
    preview: "o-spreadsheet-ChartPreview.WATERFALL_CHART",
})
    .add("pyramid", {
    displayName: _t("Population Pyramid"),
    chartSubtype: "pyramid",
    chartType: "pyramid",
    category: "misc",
    preview: "o-spreadsheet-ChartPreview.POPULATION_PYRAMID_CHART",
})
    .add("radar", {
    matcher: (definition) => definition.type === "radar" && !definition.fillArea,
    displayName: _t("Radar"),
    chartSubtype: "radar",
    chartType: "radar",
    subtypeDefinition: { fillArea: false },
    category: "misc",
    preview: "o-spreadsheet-ChartPreview.RADAR_CHART",
})
    .add("filled_radar", {
    matcher: (definition) => definition.type === "radar" && !!definition.fillArea,
    displayName: _t("Filled Radar"),
    chartType: "radar",
    chartSubtype: "filled_radar",
    subtypeDefinition: { fillArea: true },
    category: "misc",
    preview: "o-spreadsheet-ChartPreview.FILLED_RADAR_CHART",
})
    .add("geo", {
    displayName: _t("Geo Chart"),
    chartSubtype: "geo",
    chartType: "geo",
    category: "misc",
    preview: "o-spreadsheet-ChartPreview.GEO_CHART",
})
    .add("funnel", {
    displayName: _t("Funnel"),
    chartSubtype: "funnel",
    chartType: "funnel",
    category: "misc",
    preview: "o-spreadsheet-ChartPreview.FUNNEL_CHART",
})
    .add("sunburst", {
    matcher: (definition) => definition.type === "sunburst",
    displayName: _t("Sunburst"),
    chartSubtype: "sunburst",
    chartType: "sunburst",
    category: "hierarchical",
    preview: "o-spreadsheet-ChartPreview.SUNBURST_CHART",
})
    .add("treemap", {
    matcher: (definition) => definition.type === "treemap",
    displayName: _t("Tree Map"),
    chartType: "treemap",
    chartSubtype: "treemap",
    category: "hierarchical",
    preview: "o-spreadsheet-ChartPreview.TREE_MAP_CHART",
});

function generateMasterChartConfig(chartJsConfig) {
    return {
        ...chartJsConfig,
        data: {
            ...chartJsConfig.data,
            datasets: chartJsConfig.data.datasets
                .filter((ds) => !isTrendLineAxis(ds["xAxisID"]))
                .map((ds) => ({
                ...ds,
                pointRadius: ds.showLine === false ? 2 : 0, // Show points only for scatter plots
            })),
        },
        options: {
            ...chartJsConfig.options,
            hover: { mode: null },
            plugins: {
                ...chartJsConfig.options.plugins,
                title: { display: false },
                legend: { display: false },
                tooltip: { enabled: false },
                chartShowValuesPlugin: undefined,
            },
            layout: {
                padding: {
                    ...chartJsConfig.options.layout?.padding,
                    top: 5,
                    bottom: 10,
                },
            },
            scales: {
                y: {
                    ...chartJsConfig.options.scales?.y,
                    display: false,
                },
                y1: {
                    ...chartJsConfig.options.scales?.y1,
                    display: false,
                },
                x: {
                    ...chartJsConfig.options.scales?.x,
                    title: undefined,
                    ticks: {
                        ...chartJsConfig.options.scales?.x?.ticks,
                        callback: function (value) {
                            return truncateLabel(chartJsConfig.options.scales?.x?.ticks?.callback?.call(this, value), 5);
                        },
                        padding: 0,
                        font: {
                            size: 9,
                        },
                    },
                },
            },
        },
    };
}

/**
 * Create a function used to create a Chart based on the definition
 */
function chartFactory(getters) {
    const builders = chartRegistry.getAll().sort((a, b) => a.sequence - b.sequence);
    function createChart(figureId, definition, sheetId) {
        const builder = builders.find((builder) => builder.match(definition.type));
        if (!builder) {
            throw new Error(`No builder for this chart: ${definition.type}`);
        }
        return builder.createChart(definition, sheetId, getters);
    }
    return createChart;
}
/**
 * Create a function used to create a Chart Runtime based on the chart class
 * instance
 */
function chartRuntimeFactory(getters) {
    const builders = chartRegistry.getAll().sort((a, b) => a.sequence - b.sequence);
    function createRuntimeChart(chart) {
        const builder = builders.find((builder) => builder.match(chart.type));
        if (!builder) {
            throw new Error("No runtime builder for this chart.");
        }
        const runtime = builder.getChartRuntime(chart, getters);
        const definition = chart.getDefinition();
        if ("chartJsConfig" in runtime && /line|combo|bar|scatter|waterfall/.test(definition.type)) {
            const chartJsConfig = runtime.chartJsConfig;
            runtime["masterChartConfig"] = generateMasterChartConfig(chartJsConfig);
        }
        return runtime;
    }
    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(chartSheetId, definition, applyrange) {
    const transformation = chartRegistry.getAll().find((factory) => factory.match(definition.type));
    if (!transformation) {
        throw new Error("Unknown chart type.");
    }
    return transformation.transformDefinition(chartSheetId, definition, applyrange);
}

var CHART_HELPERS = /*#__PURE__*/Object.freeze({
    __proto__: null,
    AbstractChart: AbstractChart,
    BarChart: BarChart,
    CHART_AXIS_CHOICES: CHART_AXIS_CHOICES,
    CHART_COMMON_OPTIONS: CHART_COMMON_OPTIONS,
    GaugeChart: GaugeChart,
    LineChart: LineChart,
    MOVING_AVERAGE_TREND_LINE_XAXIS_ID: MOVING_AVERAGE_TREND_LINE_XAXIS_ID,
    PieChart: PieChart,
    SPREADSHEET_TO_EXCEL_TRENDLINE_TYPE_MAPPING: SPREADSHEET_TO_EXCEL_TRENDLINE_TYPE_MAPPING,
    ScorecardChart: ScorecardChart$1,
    TREND_LINE_XAXIS_ID: TREND_LINE_XAXIS_ID,
    WaterfallChart: WaterfallChart,
    adaptChartRange: adaptChartRange,
    chartFactory: chartFactory,
    chartFontColor: chartFontColor,
    chartMutedFontColor: chartMutedFontColor,
    chartRuntimeFactory: chartRuntimeFactory,
    chartToImageFile: chartToImageFile,
    chartToImageUrl: chartToImageUrl,
    checkDataset: checkDataset,
    checkLabelRange: checkLabelRange,
    createBarChartRuntime: createBarChartRuntime,
    createDataSets: createDataSets,
    createGaugeChartRuntime: createGaugeChartRuntime,
    createLineChartRuntime: createLineChartRuntime,
    createPieChartRuntime: createPieChartRuntime,
    createScorecardChartRuntime: createScorecardChartRuntime,
    createWaterfallChartRuntime: createWaterfallChartRuntime,
    drawScoreChart: drawScoreChart,
    duplicateDataSetsInDuplicatedSheet: duplicateDataSetsInDuplicatedSheet,
    duplicateLabelRangeInDuplicatedSheet: duplicateLabelRangeInDuplicatedSheet,
    formatChartDatasetValue: formatChartDatasetValue,
    formatTickValue: formatTickValue,
    getChartPositionAtCenterOfViewport: getChartPositionAtCenterOfViewport,
    getDefinedAxis: getDefinedAxis,
    getPieColors: getPieColors,
    isTrendLineAxis: isTrendLineAxis,
    shouldRemoveFirstLabel: shouldRemoveFirstLabel,
    toExcelDataset: toExcelDataset,
    toExcelLabelRange: toExcelLabelRange,
    transformChartDefinitionWithDataSetsWithZone: transformChartDefinitionWithDataSetsWithZone,
    transformDefinition: transformDefinition,
    truncateLabel: truncateLabel,
    updateChartRangesWithDataSets: updateChartRangesWithDataSets,
    validateChartDefinition: validateChartDefinition
});

function centerFigurePosition(getters, size) {
    const { scrollX, scrollY } = getters.getActiveSheetScrollInfo();
    const dim = getters.getSheetViewDimension();
    const posX = scrollX + Math.max(0, (dim.width - size.width) / 2);
    const posY = scrollY + Math.max(0, (dim.height - size.height) / 2);
    return getters.getPositionAnchorOffset({ x: posX, y: posY });
}
function getMaxFigureSize(getters, figureSize) {
    const size = deepCopy(figureSize);
    const dim = getters.getSheetViewDimension();
    const maxWidth = dim.width;
    const maxHeight = dim.height;
    if (size.width > maxWidth) {
        const ratio = maxWidth / size.width;
        size.width = maxWidth;
        size.height = size.height * ratio;
    }
    if (size.height > maxHeight) {
        const ratio = maxHeight / size.height;
        size.height = maxHeight;
        size.width = size.width * ratio;
    }
    return size;
}

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

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

function getChartMenuActions(figureId, env) {
    const chartId = env.model.getters.getChartIdFromFigureId(figureId);
    if (!chartId) {
        return [];
    }
    const menuItemSpecs = [
        {
            id: "edit",
            name: _t("Edit"),
            execute: () => {
                env.model.dispatch("SELECT_FIGURE", { figureId });
                env.openSidePanel("ChartPanel");
            },
            icon: "o-spreadsheet-Icon.EDIT",
            isEnabled: (env) => !env.isSmall,
        },
        getCopyMenuItem(figureId, env),
        getCutMenuItem(figureId, env),
        getCopyAsImageMenuItem(figureId, env),
        getDownloadChartMenuItem(figureId, env),
        getDeleteMenuItem(figureId, env),
    ];
    return createActions(menuItemSpecs).filter((action) => env.model.getters.isReadonly() ? action.isReadonlyAllowed : true);
}
function getImageMenuActions(figureId, env) {
    const menuItemSpecs = [
        getCopyMenuItem(figureId, env, _t("Image copied to clipboard")),
        getCutMenuItem(figureId, env),
        {
            id: "reset_size",
            name: _t("Reset size"),
            execute: async () => {
                const sheetId = env.model.getters.getActiveSheetId();
                const figure = env.model.getters.getFigure(sheetId, figureId);
                if (!figure) {
                    return;
                }
                const imagePath = env.model.getters.getImagePath(figureId);
                const size = env.model.getters.getImageSize(figureId) ??
                    (await env.imageProvider?.getImageOriginalSize(imagePath));
                if (!env.model.getters.getImageSize(figureId)) {
                    const image = env.model.getters.getImage(figureId);
                    image.size = size;
                }
                const { col, row } = figure;
                const { height, width } = getMaxFigureSize(env.model.getters, size);
                env.model.dispatch("UPDATE_FIGURE", {
                    sheetId,
                    figureId,
                    height,
                    width,
                    col,
                    row,
                });
            },
            icon: "o-spreadsheet-Icon.REFRESH",
        },
        {
            id: "download",
            name: _t("Download"),
            execute: async () => {
                env.model.dispatch("SELECT_FIGURE", { figureId });
                const path = env.model.getters.getImagePath(figureId);
                downloadFile(path, "image");
            },
            icon: "o-spreadsheet-Icon.DOWNLOAD",
        },
        getDeleteMenuItem(figureId, env),
    ];
    return createActions(menuItemSpecs);
}
function getCarouselMenuActions(figureId, env) {
    const isChartSelected = (env) => env.model.getters.getSelectedCarouselItem(figureId)?.type === "chart";
    const menuItemSpecs = [
        {
            id: "edit_carousel",
            name: _t("Edit carousel"),
            execute: () => {
                env.model.dispatch("SELECT_FIGURE", { figureId });
                env.openSidePanel("CarouselPanel", { figureId });
            },
            icon: "o-spreadsheet-Icon.EDIT",
            isEnabled: (env) => !env.isSmall,
        },
        {
            ...getCopyMenuItem(figureId, env, _t("Carousel copied to clipboard")),
            name: _t("Copy carousel"),
        },
        { ...getCutMenuItem(figureId, env), name: _t("Cut carousel") },
        {
            ...getDeleteMenuItem(figureId, env),
            name: _t("Delete carousel"),
            separator: true,
        },
        {
            id: "edit_chart",
            name: _t("Edit chart"),
            execute: () => {
                env.model.dispatch("SELECT_FIGURE", { figureId });
                env.openSidePanel("ChartPanel", {});
            },
            icon: "o-spreadsheet-Icon.EDIT",
            isEnabled: (env) => !env.isSmall,
            isVisible: isChartSelected,
        },
        {
            ...getCopyAsImageMenuItem(figureId, env),
            isVisible: isChartSelected,
            name: _t("Copy chart as image"),
        },
        {
            ...getDownloadChartMenuItem(figureId, env),
            isVisible: isChartSelected,
            name: _t("Download chart"),
        },
        {
            id: "popout_chart",
            name: _t("Pop out chart"),
            icon: "o-spreadsheet-Icon.EXTERNAL",
            execute: () => {
                const selectedItem = env.model.getters.getSelectedCarouselItem(figureId);
                if (!selectedItem || selectedItem.type !== "chart") {
                    return;
                }
                env.model.dispatch("POPOUT_CHART_FROM_CAROUSEL", {
                    carouselId: figureId,
                    chartId: selectedItem.chartId,
                    sheetId: env.model.getters.getActiveSheetId(),
                });
            },
            isVisible: isChartSelected,
        },
        {
            id: "delete_carousel_item",
            name: (env) => {
                const item = env.model.getters.getSelectedCarouselItem(figureId);
                return item?.type === "chart" ? _t("Delete chart") : _t("Delete data view");
            },
            execute: () => {
                const item = env.model.getters.getSelectedCarouselItem(figureId);
                if (!item) {
                    return;
                }
                const carousel = env.model.getters.getCarousel(figureId);
                const items = carousel.items.filter((itm) => !deepEquals(itm, item));
                env.model.dispatch("UPDATE_CAROUSEL", {
                    figureId,
                    sheetId: env.model.getters.getActiveSheetId(),
                    definition: { ...carousel, items },
                });
            },
            icon: "o-spreadsheet-Icon.TRASH",
            isVisible: (env) => env.model.getters.getCarousel(figureId).items.length >= 1,
        },
    ];
    return createActions(menuItemSpecs).filter((action) => env.model.getters.isReadonly() ? action.isReadonlyAllowed : true);
}
function getCopyMenuItem(figureId, env, copiedNotificationMessage) {
    return {
        id: "copy",
        name: _t("Copy"),
        description: "Ctrl+C",
        execute: async () => {
            env.model.dispatch("SELECT_FIGURE", { figureId });
            env.model.dispatch("COPY");
            const osClipboardContent = await env.model.getters.getClipboardTextAndImageContent();
            await env.clipboard.write(osClipboardContent);
            if (copiedNotificationMessage) {
                env.notifyUser({ sticky: false, type: "success", text: copiedNotificationMessage });
            }
        },
        icon: "o-spreadsheet-Icon.CLIPBOARD",
    };
}
function getCutMenuItem(figureId, env) {
    return {
        id: "cut",
        name: _t("Cut"),
        description: "Ctrl+X",
        execute: async () => {
            env.model.dispatch("SELECT_FIGURE", { figureId });
            env.model.dispatch("CUT");
            await env.clipboard.write(await env.model.getters.getClipboardTextAndImageContent());
        },
        icon: "o-spreadsheet-Icon.CUT",
    };
}
function getCopyAsImageMenuItem(figureId, env) {
    return {
        id: "copy_as_image",
        name: _t("Copy as image"),
        icon: "o-spreadsheet-Icon.COPY_AS_IMAGE",
        execute: async () => {
            const figureSheetId = env.model.getters.getFigureSheetId(figureId);
            const figure = env.model.getters.getFigure(figureSheetId, figureId);
            const chartId = env.model.getters.getChartIdFromFigureId(figureId);
            if (!chartId) {
                return;
            }
            const chartType = env.model.getters.getChartType(chartId);
            const runtime = env.model.getters.getChartRuntime(chartId);
            const imageUrl = chartToImageUrl(runtime, figure, chartType);
            const innerHTML = `<img src="${xmlEscape(imageUrl)}" />`;
            const blob = await chartToImageFile(runtime, figure, chartType);
            env.clipboard.write({
                "text/html": innerHTML,
                "image/png": blob,
            });
            env.notifyUser({ sticky: false, type: "success", text: _t("Chart copied to clipboard") });
        },
        isReadonlyAllowed: true,
    };
}
function getDownloadChartMenuItem(figureId, env) {
    return {
        id: "download",
        name: _t("Download"),
        icon: "o-spreadsheet-Icon.DOWNLOAD",
        execute: async () => {
            const figureSheetId = env.model.getters.getFigureSheetId(figureId);
            const figure = env.model.getters.getFigure(figureSheetId, figureId);
            const chartId = env.model.getters.getChartIdFromFigureId(figureId);
            if (!chartId) {
                return;
            }
            const chartType = env.model.getters.getChartType(chartId);
            const runtime = env.model.getters.getChartRuntime(chartId);
            const url = chartToImageUrl(runtime, figure, chartType);
            downloadFile(url, "chart");
        },
        isReadonlyAllowed: true,
    };
}
function getDeleteMenuItem(figureId, env) {
    return {
        id: "delete",
        name: _t("Delete"),
        execute: () => {
            env.model.dispatch("DELETE_FIGURE", {
                sheetId: env.model.getters.getActiveSheetId(),
                figureId,
            });
        },
        icon: "o-spreadsheet-Icon.TRASH",
    };
}

const CAROUSEL_DEFAULT_CHART_DEFINITION = {
    type: "bar",
    title: {},
    stacked: false,
    dataSetsHaveTitle: false,
    dataSets: [],
    legendPosition: "top",
    humanize: true,
};
function getCarouselItemPreview(getters, item) {
    if (item.type === "carouselDataView") {
        return "o-spreadsheet-Icon.DATA";
    }
    const definition = getters.getChartDefinition(item.chartId);
    const matchedChart = chartSubtypeRegistry.getAll().find((c) => c.matcher?.(definition)) ||
        chartSubtypeRegistry.get(definition.type);
    return matchedChart.preview;
}
function getCarouselItemTitle(getters, item) {
    if (item.title) {
        return item.title;
    }
    if (item.type === "carouselDataView") {
        return _t("Data");
    }
    const definition = getters.getChartDefinition(item.chartId);
    const matchedChart = chartSubtypeRegistry.getAll().find((c) => c.matcher?.(definition)) ||
        chartSubtypeRegistry.get(definition.type);
    return matchedChart.displayName;
}

class FullScreenFigureStore extends SpreadsheetStore {
    mutators = ["toggleFullScreenFigure"];
    fullScreenFigure = undefined;
    toggleFullScreenFigure(figureId) {
        if (this.fullScreenFigure?.id === figureId) {
            this.fullScreenFigure = undefined;
        }
        else {
            this.makeFullScreen(figureId);
        }
    }
    makeFullScreen(figureId) {
        const sheetId = this.getters.getActiveSheetId();
        const figure = this.getters.getFigure(sheetId, figureId);
        if (figure) {
            this.fullScreenFigure = { ...figure, x: 0, y: 0, width: 0, height: 0 };
        }
    }
}

/**
 * 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;
        }
    };
    useEffect(() => {
        intervalId = setInterval(safeCallback, delay);
        return () => clearInterval(intervalId);
    }, () => [delay]);
    return {
        pause,
        resume: () => {
            if (intervalId === undefined) {
                intervalId = setInterval(safeCallback, delay);
            }
        },
    };
}
/**
 * Calls a callback function with a time delay
 */
function useTimeOut() {
    let timeOutId;
    function clear() {
        if (timeOutId !== undefined) {
            clearTimeout(timeOutId);
            timeOutId = undefined;
        }
    }
    function schedule(callback, delay) {
        clear();
        timeOutId = setTimeout(callback, delay);
    }
    onWillUnmount(clear);
    return {
        clear,
        schedule,
    };
}

//------------------------------------------------------------------------------
// Context Menu Component
//------------------------------------------------------------------------------
css /* scss */ `
  .o-menu {
    background-color: white;
    user-select: none;

    .o-menu-item {
      height: ${DESKTOP_MENU_ITEM_HEIGHT}px;
      padding: ${MENU_ITEM_PADDING_VERTICAL}px ${MENU_ITEM_PADDING_HORIZONTAL}px;
      cursor: pointer;
      user-select: none;

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

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

      &:not(.disabled) {
        &:hover,
        &.o-menu-item-active {
          background-color: ${BUTTON_ACTIVE_BG};
          color: ${BUTTON_ACTIVE_TEXT_COLOR};
        }
        .o-menu-item-description {
          color: grey;
        }
        .o-menu-item-icon {
          .o-icon {
            color: ${ICONS_COLOR};
          }
        }
      }
      &.disabled {
        color: ${DISABLED_TEXT_COLOR};
        cursor: not-allowed;
      }
    }
  }

  .o-spreadsheet-mobile {
    .o-menu-item {
      height: ${MOBILE_MENU_ITEM_HEIGHT}px;
    }
  }
`;
class Menu extends Component {
    static template = "o-spreadsheet-Menu";
    static props = {
        menuItems: Array,
        onClose: Function,
        onClickMenu: { type: Function, optional: true },
        onMouseEnter: { type: Function, optional: true },
        onMouseOver: { type: Function, optional: true },
        onMouseLeave: { type: Function, optional: true },
        width: { type: Number, optional: true },
        isActive: { type: Function, optional: true },
        onScroll: { type: Function, optional: true },
    };
    static components = {};
    static defaultProps = {};
    hoveredMenu = undefined;
    setup() {
        onWillUnmount(() => {
            this.hoveredMenu?.onStopHover?.(this.env);
        });
    }
    get childrenHaveIcon() {
        return this.props.menuItems.some((menuItem) => menuItem !== "separator" && !!this.getIconName(menuItem));
    }
    getIconName(menu) {
        if (menu.icon(this.env)) {
            return menu.icon(this.env);
        }
        if (menu.isActive?.(this.env)) {
            return "o-spreadsheet-Icon.CHECK";
        }
        return "";
    }
    getColor(menu) {
        return cssPropertiesToCss({ color: menu.textColor });
    }
    getIconColor(menu) {
        return cssPropertiesToCss({ color: menu.iconColor });
    }
    getName(menu) {
        return menu.name(this.env);
    }
    isRoot(menu) {
        return !menu.execute;
    }
    isEnabled(menu) {
        if (menu.isEnabled(this.env)) {
            return this.env.model.getters.isReadonly() ? menu.isReadonlyAllowed : true;
        }
        return false;
    }
    get menuStyle() {
        return this.props.width ? cssPropertiesToCss({ width: this.props.width + "px" }) : "";
    }
    onMouseEnter(menu, ev) {
        this.hoveredMenu = menu;
        menu.onStartHover?.(this.env);
        this.props.onMouseEnter?.(menu, ev);
    }
    onMouseLeave(menu, ev) {
        this.hoveredMenu = undefined;
        menu.onStopHover?.(this.env);
        this.props.onMouseLeave?.(menu, ev);
    }
    onClickMenu(menu, ev) {
        if (!this.isEnabled(menu)) {
            return;
        }
        this.props.onClickMenu?.(menu, ev);
    }
}

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

/**
 * Return the o-spreadsheet element position relative
 * to the browser viewport.
 */
function useSpreadsheetRect() {
    const position = useState({ x: 0, y: 0, width: 0, height: 0 });
    let spreadsheetElement = null;
    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;
        }
    }
    onMounted(updatePosition);
    onPatched(updatePosition);
    return position;
}
/**
 * Get the rectangle inside which a popover should stay when being displayed.
 * It's the value defined in `env.getPopoverContainerRect`, or the Rect of the "o-spreadsheet"
 * element by default.
 *
 * Coordinates are expressed expressed as absolute DOM position.
 */
function usePopoverContainer() {
    const container = useState({ x: 0, y: 0, width: 0, height: 0 });
    const component = useComponent();
    const spreadsheetRect = useSpreadsheetRect();
    function updateRect() {
        const env = component.env;
        const newRect = "getPopoverContainerRect" in env && env.getPopoverContainerRect
            ? env.getPopoverContainerRect()
            : spreadsheetRect;
        container.x = newRect.x;
        container.y = newRect.y;
        container.width = newRect.width;
        container.height = newRect.height;
    }
    updateRect();
    onMounted(updateRect);
    onPatched(updateRect);
    return container;
}

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

//------------------------------------------------------------------------------
// Context MenuPopover Component
//------------------------------------------------------------------------------
css /* scss */ `
  .o-menu-wrapper {
    padding: ${MENU_VERTICAL_PADDING}px 0px;
    background-color: white;
  }
`;
const TIMEOUT_DELAY = 250;
class MenuPopover extends Component {
    static template = "o-spreadsheet-Menu-Popover";
    static props = {
        anchorRect: Object,
        popoverPositioning: { type: String, optional: true },
        menuItems: Array,
        depth: { type: Number, optional: true },
        maxHeight: { type: Number, optional: true },
        onClose: Function,
        onMenuClicked: { type: Function, optional: true },
        menuId: { type: String, optional: true },
        onMouseOver: { type: Function, optional: true },
        width: { type: Number, optional: true },
    };
    static components = { MenuPopover, Menu, Popover };
    static defaultProps = {
        depth: 1,
        popoverPositioning: "top-right",
    };
    subMenu = useState({
        isOpen: false,
        anchorRect: null,
        scrollOffset: 0,
        menuItems: [],
        isHoveringChild: false,
    });
    menuRef = useRef("menu");
    hoveredMenu = undefined;
    openingTimeOut = useTimeOut();
    setup() {
        useExternalListener(window, "click", this.onExternalClick, { capture: true });
        useExternalListener(window, "contextmenu", this.onExternalClick, { capture: true });
        onWillUpdateProps((nextProps) => {
            if (nextProps.menuItems !== this.props.menuItems) {
                this.closeSubMenu();
            }
        });
        onWillUnmount(() => {
            this.hoveredMenu?.onStopHover?.(this.env);
        });
    }
    get menuProps() {
        return {
            menuItems: this.menuItems,
            onClose: this.close.bind(this),
            onClickMenu: this.onClickMenu.bind(this),
            onMouseOver: this.onMouseOver.bind(this),
            onMouseLeave: this.onMouseLeave.bind(this),
            width: this.props.width || MENU_WIDTH,
            isActive: this.isActive.bind(this),
            onScroll: this.onScroll.bind(this),
        };
    }
    get subMenuAnchorRect() {
        const anchorRect = Object.assign({}, this.subMenu.anchorRect);
        anchorRect.y -= this.subMenu.scrollOffset || 0;
        return anchorRect;
    }
    get popoverProps() {
        const isRoot = this.props.depth === 1;
        return {
            anchorRect: {
                x: this.props.anchorRect.x,
                y: this.props.anchorRect.y,
                width: isRoot ? this.props.anchorRect.width : this.props.width || MENU_WIDTH,
                height: isRoot ? this.props.anchorRect.height : DESKTOP_MENU_ITEM_HEIGHT,
            },
            positioning: this.props.popoverPositioning,
            verticalOffset: isRoot ? 0 : MENU_VERTICAL_PADDING,
            onPopoverHidden: () => this.closeSubMenu(),
            onPopoverMoved: () => this.closeSubMenu(),
            maxHeight: this.props.maxHeight,
        };
    }
    get childrenHaveIcon() {
        return this.props.menuItems.some((menuItem) => !!this.getIconName(menuItem));
    }
    getIconName(menu) {
        if (menu.icon(this.env)) {
            return menu.icon(this.env);
        }
        if (menu.isActive?.(this.env)) {
            return "o-spreadsheet-Icon.CHECK";
        }
        return "";
    }
    getColor(menu) {
        return cssPropertiesToCss({ color: menu.textColor });
    }
    getIconColor(menu) {
        return cssPropertiesToCss({ color: menu.iconColor });
    }
    async activateMenu(menu, isMiddleClick) {
        const result = await menu.execute?.(this.env, isMiddleClick);
        this.close();
        this.props.onMenuClicked?.({ detail: result });
    }
    close() {
        this.closeSubMenu();
        this.props.onClose();
    }
    onExternalClick(ev) {
        // Don't close a root menu when clicked to open the submenus.
        const el = this.menuRef.el;
        if (el && getOpenedMenus().some((el) => isChildEvent(el, ev))) {
            return;
        }
        ev.closedMenuId = this.props.menuId;
        this.close();
    }
    get menuItems() {
        return getMenuItemsAndSeparators(this.env, this.props.menuItems);
    }
    getName(menu) {
        return menu.name(this.env);
    }
    isRoot(menu) {
        return !menu.execute;
    }
    isEnabled(menu) {
        if (menu.isEnabled(this.env)) {
            return this.env.model.getters.isReadonly() ? menu.isReadonlyAllowed : true;
        }
        return false;
    }
    isActive(menuItem) {
        return (this.subMenu?.isHoveringChild || false) && this.isParentMenu(this.subMenu, menuItem);
    }
    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, parentMenuEl) {
        if (!parentMenuEl) {
            return;
        }
        const y = parentMenuEl.getBoundingClientRect().top;
        this.subMenu.anchorRect = {
            x: getRefBoundingRect(this.menuRef).x,
            y: y - (this.subMenu.scrollOffset || 0),
            width: this.props.width || MENU_WIDTH,
            height: DESKTOP_MENU_ITEM_HEIGHT,
        };
        this.subMenu.menuItems = menu.children(this.env);
        this.subMenu.isOpen = true;
        this.subMenu.parentMenu = menu;
    }
    isParentMenu(subMenu, menuItem) {
        return subMenu.parentMenu?.id === menuItem.id;
    }
    closeSubMenu() {
        if (this.subMenu.isHoveringChild) {
            return;
        }
        this.subMenu.isOpen = false;
        this.subMenu.parentMenu = undefined;
    }
    onClickMenu(menu, ev) {
        if (this.isRoot(menu)) {
            this.openSubMenu(menu, ev.currentTarget);
        }
        else {
            this.activateMenu(menu, isMiddleClickOrCtrlClick(ev));
        }
    }
    onMouseOver(menu, ev) {
        if (this.isEnabled(menu)) {
            if (this.isParentMenu(this.subMenu, menu)) {
                this.openingTimeOut.clear();
                return;
            }
            const currentTarget = ev.currentTarget;
            if (this.isRoot(menu)) {
                this.openingTimeOut.schedule(() => {
                    this.openSubMenu(menu, currentTarget);
                }, TIMEOUT_DELAY);
            }
        }
    }
    onMouseOverMainMenu() {
        this.props.onMouseOver?.();
        this.subMenu.isHoveringChild = false;
    }
    onMouseOverChildMenu() {
        this.subMenu.isHoveringChild = true;
        this.openingTimeOut.clear();
    }
    onMouseLeave(menu) {
        this.openingTimeOut.schedule(this.closeSubMenu.bind(this), TIMEOUT_DELAY);
    }
    get menuStyle() {
        return this.props.width ? cssPropertiesToCss({ width: this.props.width + "px" }) : "";
    }
}

class ChartDashboardMenu extends Component {
    static template = "o-spreadsheet-ChartDashboardMenu";
    static components = { MenuPopover };
    static props = { chartId: String, hasFullScreenButton: { type: Boolean, optional: true } };
    static defaultProps = { hasFullScreenButton: true };
    fullScreenFigureStore;
    menuState = useState({ isOpen: false, anchorRect: null, menuItems: [] });
    setup() {
        super.setup();
        this.fullScreenFigureStore = useStore(FullScreenFigureStore);
    }
    getMenuItems() {
        return [this.fullScreenMenuItem].filter(isDefined);
    }
    get backgroundColor() {
        const color = this.env.model.getters.getChartDefinition(this.props.chartId).background;
        return "background-color: " + (color || BACKGROUND_CHART_COLOR);
    }
    openContextMenu(ev) {
        this.menuState.isOpen = true;
        this.menuState.anchorRect = getBoundingRectAsPOJO(ev.currentTarget);
        const figureId = this.env.model.getters.getFigureIdFromChartId(this.props.chartId);
        this.menuState.menuItems = getChartMenuActions(figureId, this.env);
    }
    get fullScreenMenuItem() {
        if (!this.props.hasFullScreenButton) {
            return undefined;
        }
        const definition = this.env.model.getters.getChartDefinition(this.props.chartId);
        const figureId = this.env.model.getters.getFigureIdFromChartId(this.props.chartId);
        if (definition.type === "scorecard") {
            return undefined;
        }
        const isFullScreen = figureId === this.fullScreenFigureStore.fullScreenFigure?.id;
        return {
            id: "fullScreenChart",
            label: isFullScreen ? _t("Exit Full Screen") : _t("Full Screen"),
            class: `text-muted fa ${isFullScreen ? "fa-compress" : "fa-expand"}`,
            onClick: () => {
                this.fullScreenFigureStore.toggleFullScreenFigure(figureId);
            },
        };
    }
}

class CarouselFigure extends Component {
    static template = "o-spreadsheet-CarouselFigure";
    static props = {
        figureUI: Object,
        editFigureStyle: { type: Function, optional: true },
        isFullScreen: { type: Boolean, optional: true },
        openContextMenu: { type: Function, optional: true },
    };
    static components = { ChartDashboardMenu, MenuPopover };
    carouselTabsRef = useRef("carouselTabs");
    carouselTabsDropdownRef = useRef("carouselTabsDropdown");
    menuState = useState({ isOpen: false, anchorRect: null, menuItems: [] });
    hiddenItems = [];
    animationStore;
    fullScreenFigureStore;
    setup() {
        this.animationStore = useStore(ChartAnimationStore);
        this.fullScreenFigureStore = useStore(FullScreenFigureStore);
        useEffect(() => {
            if (this.selectedCarouselItem?.type === "carouselDataView") {
                this.props.editFigureStyle?.({ "pointer-events": "none" });
            }
            else {
                this.props.editFigureStyle?.({ "pointer-events": "auto" });
            }
            this.updateTabsVisibility();
        });
    }
    get carousel() {
        return this.env.model.getters.getCarousel(this.props.figureUI.id);
    }
    get selectedCarouselItem() {
        return this.env.model.getters.getSelectedCarouselItem(this.props.figureUI.id);
    }
    get chartComponent() {
        const selectedItem = this.selectedCarouselItem;
        if (selectedItem?.type !== "chart") {
            throw new Error("Selected item is not a chart");
        }
        const type = this.env.model.getters.getChartType(selectedItem.chartId);
        const component = chartComponentRegistry.get(type);
        if (!component) {
            throw new Error(`Component is not defined for type ${type}`);
        }
        return component;
    }
    onCarouselDoubleClick() {
        this.env.model.dispatch("SELECT_FIGURE", { figureId: this.props.figureUI.id });
        this.env.openSidePanel("CarouselPanel", { figureId: this.props.figureUI.id });
    }
    isItemSelected(item) {
        const selectedItem = this.selectedCarouselItem;
        return deepEquals(selectedItem, item);
    }
    getItemTitle(item) {
        return getCarouselItemTitle(this.env.model.getters, item);
    }
    onCarouselTabClick(item) {
        this.env.model.dispatch("UPDATE_CAROUSEL_ACTIVE_ITEM", {
            figureId: this.props.figureUI.id,
            sheetId: this.env.model.getters.getActiveSheetId(),
            item,
        });
        if (item.type === "chart") {
            const animationChartId = item.chartId + (this.props.isFullScreen ? "-fullscreen" : "");
            this.animationStore?.enableAnimationForChart(animationChartId);
        }
    }
    get headerStyle() {
        const cssProperties = {};
        if (this.selectedCarouselItem?.type === "chart") {
            const chart = this.env.model.getters.getChartRuntime(this.selectedCarouselItem.chartId);
            cssProperties["background-color"] = chart.background;
        }
        else {
            cssProperties["background-color"] = "#ffffff";
        }
        return cssPropertiesToCss(cssProperties);
    }
    get title() {
        return this.carousel.title?.text ?? "";
    }
    get titleStyle() {
        const style = { ...DEFAULT_CAROUSEL_TITLE_STYLE, ...this.carousel.title };
        return cssPropertiesToCss(cellTextStyleToCss(chartStyleToCellStyle(style)));
    }
    updateTabsVisibility() {
        const tabsContainerEl = this.carouselTabsRef.el;
        const dropDownEl = this.carouselTabsDropdownRef.el;
        if (!tabsContainerEl || !dropDownEl) {
            return;
        }
        this.hiddenItems = [];
        const containerRect = getBoundingRectAsPOJO(tabsContainerEl);
        const tabs = Array.from(tabsContainerEl.children);
        for (const tab of tabs) {
            tab.style.display = "block";
        }
        const tabWidths = tabs.map((tab) => getBoundingRectAsPOJO(tab).width);
        let currentWidth = 0;
        for (let i = 0; i < tabs.length; i++) {
            const shouldBeHidden = currentWidth + tabWidths[i] > containerRect.width;
            currentWidth += tabWidths[i];
            if (shouldBeHidden) {
                tabs[i].style.display = "none";
                this.hiddenItems.push(this.carousel.items[i]);
            }
        }
        dropDownEl.style.display = this.hiddenItems.length ? "block" : "none";
    }
    get menuId() {
        return "carousel-tabs-menu-";
    }
    toggleMenu(ev) {
        if (ev.closedMenuId === this.menuId) {
            this.menuState.isOpen = false;
            return;
        }
        const rect = getRefBoundingRect(this.carouselTabsDropdownRef);
        const menuItems = this.hiddenItems.map((item) => ({
            name: this.getItemTitle(item),
            execute: () => this.onCarouselTabClick(item),
            isActive: () => this.isItemSelected(item),
            isReadonlyAllowed: true,
        }));
        this.menuState.isOpen = true;
        this.menuState.anchorRect = rect;
        this.menuState.menuItems = createActions(menuItems);
    }
    toggleFullScreen() {
        if (this.selectedCarouselItem?.type === "chart") {
            this.fullScreenFigureStore.toggleFullScreenFigure(this.props.figureUI.id);
        }
    }
    get fullScreenButtonTitle() {
        return this.props.isFullScreen ? _t("Exit Full Screen") : _t("Full Screen");
    }
    get visibleCarouselItems() {
        return this.carousel.items.filter((item) => item.type === "carouselDataView" && this.props.isFullScreen ? false : true);
    }
    openContextMenu(event) {
        const target = event.currentTarget;
        if (target) {
            this.props.openContextMenu?.(getBoundingRectAsPOJO(target));
        }
    }
}

class ChartFigure extends Component {
    static template = "o-spreadsheet-ChartFigure";
    static props = {
        figureUI: Object,
        editFigureStyle: { type: Function, optional: true },
        isFullScreen: { type: Boolean, optional: true },
        openContextMenu: { type: Function, optional: true },
    };
    static components = { ChartDashboardMenu };
    onDoubleClick() {
        this.env.model.dispatch("SELECT_FIGURE", { figureId: this.props.figureUI.id });
        this.env.openSidePanel("ChartPanel");
    }
    get chartType() {
        return this.env.model.getters.getChartType(this.chartId);
    }
    get chartId() {
        const chartId = this.env.model.getters.getChartIdFromFigureId(this.props.figureUI.id);
        if (!chartId) {
            throw new Error(`No chart found for figure ID: ${this.props.figureUI.id}`);
        }
        return chartId;
    }
    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;
    }
}

class ImageFigure extends Component {
    static template = "o-spreadsheet-ImageFigure";
    static props = {
        figureUI: Object,
        editFigureStyle: { type: Function, optional: true },
        openContextMenu: { type: Function, optional: true },
    };
    static components = {};
    // ---------------------------------------------------------------------------
    // Getters
    // ---------------------------------------------------------------------------
    get figureId() {
        return this.props.figureUI.id;
    }
    get getImagePath() {
        return this.env.model.getters.getImagePath(this.figureId);
    }
}

const figureRegistry = new Registry();
figureRegistry.add("chart", {
    Component: ChartFigure,
    SidePanelComponent: "ChartPanel",
    menuBuilder: getChartMenuActions,
});
figureRegistry.add("image", {
    Component: ImageFigure,
    keepRatio: true,
    minFigSize: 20,
    borderWidth: 0,
    menuBuilder: getImageMenuActions,
});
figureRegistry.add("carousel", {
    Component: CarouselFigure,
    menuBuilder: getCarouselMenuActions,
});

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

    &:focus {
      outline: none;
    }
  }

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

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

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

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

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

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

    .o-figure.active:focus,
    .o-figure:hover {
      .o-figure-menu {
        display: flex;
      }
    }
  }
`;
class FigureComponent extends Component {
    static template = "o-spreadsheet-FigureComponent";
    static props = {
        figureUI: Object,
        style: { type: String, optional: true },
        class: { type: String, optional: true },
        onMouseDown: { type: Function, optional: true },
        onClickAnchor: { type: Function, optional: true },
    };
    static components = { MenuPopover };
    static defaultProps = {
        onMouseDown: () => { },
        onClickAnchor: () => { },
    };
    menuState = useState({ isOpen: false, anchorRect: null, menuItems: [] });
    figureRef = useRef("figure");
    figureWrapperRef = useRef("figureWrapper");
    menuButtonRef = useRef("menuButton");
    borderWidth;
    get isSelected() {
        return this.env.model.getters.getSelectedFigureId() === this.props.figureUI.id;
    }
    get figureRegistry() {
        return figureRegistry;
    }
    getBorderWidth() {
        if (this.env.isDashboard())
            return 0;
        return this.isSelected ? ACTIVE_BORDER_WIDTH : this.borderWidth;
    }
    getBorderStyle(position) {
        const borderWidth = this.getBorderWidth();
        const borderColor = this.isSelected ? SELECTION_BORDER_COLOR : FIGURE_BORDER_COLOR;
        return `border-${position}: ${borderWidth}px solid ${borderColor};`;
    }
    get wrapperStyle() {
        const { x, y, width, height } = this.props.figureUI;
        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;
        const style = {};
        if (resizer.includes("top")) {
            style.top = `${-anchorCenteringOffset}px`;
        }
        else if (resizer.includes("bottom")) {
            style.bottom = `${-anchorCenteringOffset}px`;
        }
        else {
            style.bottom = `calc(50% - ${anchorCenteringOffset}px)`;
        }
        if (resizer.includes("left")) {
            style.left = `${-anchorCenteringOffset}px`;
        }
        else if (resizer.includes("right")) {
            style.right = `${-anchorCenteringOffset}px`;
        }
        else {
            style.right = `calc(50% - ${anchorCenteringOffset}px)`;
        }
        return cssPropertiesToCss(style);
    }
    setup() {
        const borderWidth = figureRegistry.get(this.props.figureUI.tag).borderWidth;
        this.borderWidth = borderWidth !== undefined ? borderWidth : BORDER_WIDTH;
        useEffect((selectedFigureId, thisFigureId, el) => {
            if (selectedFigureId === thisFigureId) {
                /** Scrolling on a newly inserted figure that overflows outside the viewport
                 * will break the whole layout.
                 * NOTE: `preventScroll`does not work on mobile but then again,
                 * mobile is not really supported ATM.
                 *
                 * TODO: When implementing proper mobile, we will need to scroll the viewport
                 * correctly (and render?) before focusing the element.
                 */
                el?.focus({ preventScroll: true });
            }
        }, () => [
            this.env.model.getters.getSelectedFigureId(),
            this.props.figureUI.id,
            this.figureRef.el,
        ]);
    }
    clickAnchor(dirX, dirY, ev) {
        this.props.onClickAnchor(dirX, dirY, ev);
    }
    onMouseDown(ev) {
        if (!this.env.isMobile()) {
            this.props.onMouseDown(ev);
        }
    }
    onClick(ev) {
        if (this.env.isMobile()) {
            this.props.onMouseDown(ev);
        }
    }
    onKeyDown(ev) {
        const keyDownShortcut = keyboardEventToShortcutString(ev);
        switch (keyDownShortcut) {
            case "Delete":
            case "Backspace":
                this.env.model.dispatch("DELETE_FIGURE", {
                    sheetId: this.env.model.getters.getActiveSheetId(),
                    figureId: this.props.figureUI.id,
                });
                ev.preventDefault();
                ev.stopPropagation();
                break;
            case "ArrowDown":
            case "ArrowLeft":
            case "ArrowRight":
            case "ArrowUp":
                const { col, row, offset } = this.postionInBoundary(this.props.figureUI, ev.key);
                this.env.model.dispatch("UPDATE_FIGURE", {
                    sheetId: this.env.model.getters.getActiveSheetId(),
                    figureId: this.props.figureUI.id,
                    offset,
                    col,
                    row,
                });
                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;
        }
    }
    postionInBoundary(position, key) {
        const sheetId = this.env.model.getters.getActiveSheetId();
        let { col, row, offset } = position;
        offset = { ...offset };
        switch (key) {
            case "ArrowUp":
                if (offset.y === 0) {
                    row--;
                    offset.y = this.env.model.getters.getRowSize(sheetId, row) - 1;
                }
                else {
                    offset.y--;
                }
                break;
            case "ArrowLeft":
                if (offset.x === 0) {
                    col--;
                    offset.x = this.env.model.getters.getColSize(sheetId, col) - 1;
                }
                else {
                    offset.x--;
                }
                break;
            case "ArrowDown":
                if (offset.y === this.env.model.getters.getRowSize(sheetId, row)) {
                    row++;
                    offset.y = 0;
                }
                else {
                    offset.y++;
                }
                break;
            case "ArrowRight":
                if (offset.x === this.env.model.getters.getColSize(sheetId, row)) {
                    col++;
                    offset.x = 0;
                }
                else {
                    offset.x++;
                }
        }
        return { col, row, offset };
    }
    onContextMenu(ev) {
        if (this.env.isDashboard())
            return;
        this.openContextMenu({ x: ev.clientX, y: ev.clientY, width: 0, height: 0 });
    }
    showMenu() {
        this.openContextMenu(getRefBoundingRect(this.menuButtonRef));
    }
    openContextMenu(anchorRect) {
        this.menuState.isOpen = true;
        this.menuState.anchorRect = anchorRect;
        this.menuState.menuItems = figureRegistry
            .get(this.props.figureUI.tag)
            .menuBuilder(this.props.figureUI.id, this.env);
    }
    editWrapperStyle(properties) {
        if (this.figureWrapperRef.el) {
            for (const property in properties) {
                this.figureWrapperRef.el.style.setProperty(property, properties[property] || null);
            }
        }
    }
}

class DelayedHoveredCellStore extends SpreadsheetStore {
    mutators = ["clear", "hover"];
    col;
    row;
    handle(cmd) {
        switch (cmd.type) {
            case "ACTIVATE_SHEET":
                this.clear();
        }
    }
    hover(position) {
        if (position.col === this.col && position.row === this.row) {
            return "noStateChange";
        }
        this.col = position.col;
        this.row = position.row;
        return;
    }
    clear() {
        if (this.col === undefined && this.row === undefined) {
            return "noStateChange";
        }
        this.col = undefined;
        this.row = undefined;
        return;
    }
}

const cellPopoverRegistry = new Registry();

class CellPopoverStore extends SpreadsheetStore {
    mutators = ["open", "close"];
    persistentPopover;
    hoveredCell = this.get(DelayedHoveredCellStore);
    handle(cmd) {
        switch (cmd.type) {
            case "ACTIVATE_SHEET":
                this.close();
        }
    }
    open({ col, row }, type) {
        const sheetId = this.getters.getActiveSheetId();
        if (!cellPopoverRegistry.contains(type)) {
            return;
        }
        this.persistentPopover = { col, row, sheetId, type };
    }
    close() {
        if (!this.persistentPopover) {
            return "noStateChange";
        }
        this.persistentPopover = undefined;
        return;
    }
    get persistentCellPopover() {
        return ((this.persistentPopover && { isOpen: true, ...this.persistentPopover }) || { isOpen: false });
    }
    get isOpen() {
        return this.persistentPopover !== undefined;
    }
    get cellPopover() {
        const sheetId = this.getters.getActiveSheetId();
        if (this.persistentPopover && this.getters.isVisibleInViewport(this.persistentPopover)) {
            const position = this.getters.getMainCellPosition(this.persistentPopover);
            const popover = cellPopoverRegistry
                .get(this.persistentPopover.type)
                .onOpen?.(position, this.getters);
            return !popover?.isOpen
                ? { isOpen: false }
                : {
                    ...popover,
                    anchorRect: this.computePopoverAnchorRect(this.persistentPopover),
                };
        }
        const { col, row } = this.hoveredCell;
        if (col === undefined ||
            row === undefined ||
            !this.getters.isVisibleInViewport({ sheetId, col, row })) {
            return { isOpen: false };
        }
        const position = this.getters.getMainCellPosition({ sheetId, col, row });
        const popover = cellPopoverRegistry
            .getAll()
            .map((matcher) => matcher.onHover?.(position, this.getters))
            .find((popover) => popover?.isOpen);
        return !popover?.isOpen
            ? { isOpen: false }
            : {
                ...popover,
                anchorRect: this.computePopoverAnchorRect(position),
            };
    }
    computePopoverAnchorRect({ col, row }) {
        const sheetId = this.getters.getActiveSheetId();
        const merge = this.getters.getMerge({ sheetId, col, row });
        if (merge) {
            return this.getters.getVisibleRect(merge);
        }
        return this.getters.getVisibleRect(positionToZone({ col, row }));
    }
}

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

    .o-error-tooltip-message {
      overflow: hidden;
    }
  }
`;
class ErrorToolTip extends Component {
    static maxSize = { maxHeight: ERROR_TOOLTIP_MAX_HEIGHT };
    static template = "o-spreadsheet-ErrorToolTip";
    static props = {
        cellPosition: Object,
        onClosed: { type: Function, optional: true },
    };
    get dataValidationErrorMessage() {
        return this.env.model.getters.getInvalidDataValidationMessage(this.props.cellPosition);
    }
    get evaluationError() {
        const cell = this.env.model.getters.getEvaluatedCell(this.props.cellPosition);
        if (cell.message) {
            return cell;
        }
        return undefined;
    }
    get errorOriginPositionString() {
        if (this.env.model.getters.isDashboard()) {
            return "";
        }
        const evaluationError = this.evaluationError;
        const position = evaluationError?.errorOriginPosition;
        if (!position || deepEquals(position, this.props.cellPosition)) {
            return "";
        }
        const sheetId = position.sheetId;
        return this.env.model.getters.getRangeString(this.env.model.getters.getRangeFromZone(sheetId, positionToZone(position)), this.env.model.getters.getActiveSheetId());
    }
    selectCell() {
        const position = this.evaluationError?.errorOriginPosition;
        if (!position) {
            return;
        }
        const activeSheetId = this.env.model.getters.getActiveSheetId();
        if (position.sheetId !== activeSheetId) {
            this.env.model.dispatch("ACTIVATE_SHEET", {
                sheetIdFrom: activeSheetId,
                sheetIdTo: position.sheetId,
            });
        }
        this.env.model.selection.selectCell(position.col, position.row);
    }
}
const ErrorToolTipPopoverBuilder = {
    onHover: (position, getters) => {
        const cell = getters.getEvaluatedCell(position);
        if ((cell.type === CellValueType.error && !!cell.message) ||
            getters.getInvalidDataValidationMessage(position)) {
            return {
                isOpen: true,
                props: {
                    cellPosition: position,
                },
                Component: ErrorToolTip,
                cellCorner: "top-right",
            };
        }
        return { isOpen: false };
    },
};

class Collapse extends Component {
    static template = "o-spreadsheet-Collapse";
    static props = {
        isCollapsed: Boolean,
        slots: Object,
    };
    contentRef = useRef("content");
    setup() {
        onMounted(() => {
            if (this.props.isCollapsed) {
                this.contentRef.el?.classList.add("d-none");
            }
        });
        onWillUpdateProps((nextProps) => {
            if (nextProps.isCollapsed !== this.props.isCollapsed) {
                this.startTransition(nextProps.isCollapsed);
            }
        });
    }
    startTransition(isCollapsed) {
        const el = this.contentRef.el;
        if (!el) {
            return;
        }
        el.classList.remove("d-none");
        el.classList.add("overflow-hidden");
        const startHeight = isCollapsed ? el.scrollHeight : 0;
        const endHeight = isCollapsed ? 0 : el.scrollHeight;
        const animation = el.animate([{ maxHeight: startHeight + "px" }, { maxHeight: endHeight + "px" }], { duration: 350, easing: "ease" });
        animation.onfinish = () => {
            el.classList.remove("overflow-hidden");
            if (this.props.isCollapsed) {
                el.classList.add("d-none");
            }
        };
    }
}

css /* scss */ `
  .o_side_panel_collapsible_title {
    font-size: 16px;
    cursor: pointer;
    padding: 6px 0px 6px 6px !important;

    .collapsor-arrow {
      transform: rotate(-90deg);
      display: inline-block;
      transform-origin: 8px 11px;
      transition: transform 0.2s ease-in-out;

      .o-icon {
        width: 16px;
        height: 22px;
      }
    }
    .collapsor:not(.collapsed) .collapsor-arrow {
      transform: rotate(0);
    }

    .collapsor {
      width: 100%;
      transition-delay: 0.35s;
      transition-property: all;
    }
  }
`;
class SidePanelCollapsible extends Component {
    static template = "o-spreadsheet-SidePanelCollapsible";
    static props = {
        slots: Object,
        title: { type: String, optional: true },
        isInitiallyCollapsed: { type: Boolean, optional: true },
        class: { type: String, optional: true },
    };
    static components = { Collapse };
    state = useState({
        isCollapsed: this.props.isInitiallyCollapsed,
    });
    toggle() {
        this.state.isCollapsed = !this.state.isCollapsed;
    }
}

class CriterionForm extends Component {
    static props = {
        criterion: Object,
        onCriterionChanged: Function,
        disableFormulas: { type: Boolean, optional: true },
    };
    setup() {
        const composerFocusStore = useStore(ComposerFocusStore);
        onMounted(() => {
            composerFocusStore.activeComposer.stopEdition();
        });
    }
    updateCriterion(criterion) {
        const filteredCriterion = {
            ...this.props.criterion,
            ...criterion,
        };
        this.props.onCriterionChanged(filteredCriterion);
    }
}

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

class DOMFocusableElementStore {
    mutators = ["setFocusableElement", "focus"];
    focusableElement = undefined;
    setFocusableElement(element) {
        this.focusableElement = element;
        return "noStateChange";
    }
    focus() {
        if (this.focusableElement === document.activeElement) {
            return "noStateChange";
        }
        this.focusableElement?.focus();
        return;
    }
}

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

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

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

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

class ContentEditableHelper {
    // todo make el private and expose dedicated methods
    el;
    constructor(el) {
        this.el = el;
    }
    updateEl(el) {
        this.el = el;
    }
    /**
     * select the text at position start to end, no matter the children
     */
    selectRange(start, end) {
        const selection = window.getSelection();
        const { start: currentStart, end: currentEnd } = this.getCurrentSelection();
        if (currentStart === start && currentEnd === end) {
            return;
        }
        if (selection.rangeCount === 0) {
            const range = document.createRange();
            selection.addRange(range);
        }
        const currentRange = selection.getRangeAt(0);
        let range;
        if (this.el.contains(currentRange.startContainer)) {
            range = currentRange;
        }
        else {
            range = document.createRange();
            selection.removeAllRanges();
            selection.addRange(range);
        }
        if (start === end && start === 0) {
            range.setStart(this.el, 0);
            range.setEnd(this.el, 0);
        }
        else {
            const textLength = this.getText().length;
            if (start < 0 || end > textLength) {
                console.warn(`wrong selection asked start ${start}, end ${end}, text content length ${textLength}`);
                if (start < 0)
                    start = 0;
                if (end > textLength)
                    end = textLength;
                if (start > textLength)
                    start = textLength;
            }
            const startNode = this.findChildAtCharacterIndex(start);
            const endNode = this.findChildAtCharacterIndex(end);
            // setEnd (setStart) will result in a collapsed range if the end point is before the start point
            // https://developer.mozilla.org/en-US/docs/Web/API/Range/setEnd
            if (start <= end) {
                range.setStart(startNode.node, startNode.offset);
                range.setEnd(endNode.node, endNode.offset);
            }
            else {
                range.setStart(endNode.node, endNode.offset);
                range.setEnd(startNode.node, startNode.offset);
            }
        }
    }
    /**
     * finds the dom element that contains the character at `offset`
     */
    findChildAtCharacterIndex(offset) {
        const it = iterateChildren(this.el);
        let current, previous;
        let usedCharacters = offset;
        let isFirstParagraph = true;
        do {
            current = it.next();
            if (!current.done && !current.value.hasChildNodes()) {
                if (current.value.textContent && current.value.textContent.length < usedCharacters) {
                    usedCharacters -= current.value.textContent.length;
                }
                else if (current.value.textContent &&
                    current.value.textContent.length >= usedCharacters) {
                    it.return(current.value);
                }
                previous = current.value;
            }
            // One new paragraph = one new line character, except for the first paragraph
            if (!current.done && current.value.nodeName === "P") {
                if (isFirstParagraph) {
                    isFirstParagraph = false;
                }
                else {
                    usedCharacters--;
                }
            }
        } while (!current.done && usedCharacters);
        if (current.value) {
            return { node: current.value, offset: usedCharacters };
        }
        return { node: previous, offset: usedCharacters };
    }
    /**
     * Sets (or Replaces all) the text inside the root element in the form of distinctive paragraphs and
     * span for each element provided in `contents`.
     *
     * The function will apply the diff between the current content and the new content to avoid the systematic
     * destruction of DOM elements which interferes with IME[1]
     *
     * Each line of text will be encapsulated in a paragraph element.
     * Each span will have its own fontcolor and specific class if provided in the HtmlContent object.
     *
     * [1] https://developer.mozilla.org/en-US/docs/Glossary/Input_method_editor
     */
    setText(contents) {
        if (contents.length === 0) {
            this.removeAll();
            return;
        }
        const childElements = Array.from(this.el.childNodes);
        const contentLength = contents.length;
        for (let i = 0; i < contentLength; i++) {
            const line = contents[i];
            const childElement = childElements[i];
            let newChild = false;
            let p;
            if (childElement && childElement.nodeName === "P") {
                p = childElement;
            }
            else {
                newChild = true;
                p = document.createElement("p");
            }
            const lineLength = line.length;
            const existingChildren = Array.from(p.childNodes);
            for (let j = 0; j < lineLength; j++) {
                const content = line[j];
                const child = existingChildren[j];
                // child nodes can be multiple types of nodes: Span, Text, Div, etc...
                // We can only modify a node in place if it has the same type as the content
                // that we would insert, which are spans.
                // Otherwise, it means that the node has been input by the user, through the keyboard or a copy/paste
                const childIsSpan = child && "tagName" in child && child.tagName === "SPAN";
                if (childIsSpan && compareContentToSpanElement(content, child)) {
                    continue;
                }
                // this is an empty line in the content
                if (!content.value && !content.classes?.length) {
                    if (child)
                        p.removeChild(child);
                    continue;
                }
                const span = document.createElement("span");
                span.innerText = content.value;
                span.style.color = content.color || "";
                span.addEventListener("mousemove", () => {
                    content.onHover?.(getBoundingRectAsPOJO(span));
                });
                span.addEventListener("mouseleave", () => {
                    content.onStopHover?.();
                });
                span.classList.add(...(content.classes || []));
                if (child) {
                    p.replaceChild(span, child);
                }
                else {
                    p.appendChild(span);
                }
            }
            if (existingChildren.length > lineLength) {
                for (let i = lineLength; i < existingChildren.length; i++) {
                    p.removeChild(existingChildren[i]);
                }
            }
            // Empty line
            if (!p.hasChildNodes()) {
                p.appendChild(document.createElement("span"));
            }
            // replace p if necessary
            if (newChild) {
                if (childElement) {
                    this.el.replaceChild(p, childElement);
                }
                else {
                    this.el.appendChild(p);
                }
            }
        }
        if (childElements.length > contentLength) {
            for (let i = contentLength; i < childElements.length; i++) {
                this.el.removeChild(childElements[i]);
            }
        }
    }
    scrollSelectionIntoView() {
        const focusedNode = document.getSelection()?.focusNode;
        if (!focusedNode || !this.el.contains(focusedNode))
            return;
        const element = focusedNode instanceof HTMLElement ? focusedNode : focusedNode.parentElement;
        element?.scrollIntoView?.({ block: "nearest" });
    }
    /**
     * remove the current selection of the user
     * */
    removeSelection() {
        const 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() {
        return getCurrentSelection(this.el);
    }
    getText() {
        let text = "";
        let isFirstParagraph = true;
        let emptyParagraph = false;
        const it = iterateChildren(this.el);
        for (let current = it.next(); !current.done; current = it.next()) {
            if (current.value.nodeName === "P" ||
                (current.value.nodeName === "DIV" && current.value !== this.el) // On paste, the HTML may contain <div> instead of <p>
            ) {
                if (isFirstParagraph) {
                    isFirstParagraph = false;
                }
                else {
                    text += NEWLINE;
                }
                emptyParagraph = isEmptyParagraph(current.value);
                continue;
            }
            if (!current.value.hasChildNodes()) {
                if (current.value.nodeName === "BR" && !emptyParagraph) {
                    text += NEWLINE;
                }
                text += current.value.textContent;
            }
        }
        return text;
    }
}
function compareContentToSpanElement(content, node) {
    const contentColor = content.color ? toHex(content.color) : "";
    const nodeColor = node.style?.color ? toHex(node.style.color) : "";
    const sameColor = contentColor === nodeColor;
    const sameClass = deepEquals(content.classes, [...node.classList]);
    const sameContent = node.innerText === content.value;
    return sameColor && sameClass && sameContent;
}
const doc = new DOMParser();
const brNode = doc.parseFromString("<br>", "text/html").body.firstChild;
const spanBrNode = doc.parseFromString("<span><br></span>", "text/html").body.firstChild;
function isEmptyParagraph(node) {
    if (node.childNodes.length > 1)
        return false;
    const node2 = node.firstChild?.cloneNode(true);
    if (!node2)
        return true;
    if (!(node2 instanceof Element))
        return false;
    node2.removeAttribute("class");
    node2.removeAttribute("style");
    return node2.isEqualNode(brNode) || node2.isEqualNode(spanBrNode) || false;
}

class FunctionDescriptionProvider extends Component {
    static template = "o-spreadsheet-FunctionDescriptionProvider";
    static props = {
        functionDescription: Object,
        argsToFocus: Array,
    };
    static components = { Collapse };
    state = useState({
        isCollapsed: true,
    });
    toggle() {
        this.state.isCollapsed = !this.state.isCollapsed;
    }
    getContext() {
        return this.props;
    }
    get formulaArgSeparator() {
        return this.env.model.getters.getLocale().formulaArgSeparator + " ";
    }
}

const BUBBLE_ARROW_SIZE = 7;
css /* scss */ `
  .o-spreadsheet {
    .o-speech-bubble {
      background-color: white;
      box-sizing: border-box;
      box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.15);
      border: 1px solid #ccc;
      z-index: ${ComponentsImportance.Popover};

      &::after {
        content: "";
        position: absolute;
        top: 100%;
        left: 50%;
        background-color: white;
        height: ${BUBBLE_ARROW_SIZE}px;
        width: ${BUBBLE_ARROW_SIZE}px;
        transform-origin: top left;
        transform: translate(0, -67%) rotate(45deg);
        border-right: 1px solid #ccc;
        border-bottom: 1px solid #ccc;
        box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.15);
      }
    }

    .o-speech-content {
      max-width: 300px;
    }
  }
`;
class SpeechBubble extends Component {
    static template = "o-spreadsheet-SpeechBubble";
    static props = { content: String, anchorRect: Object };
    static components = {};
    spreadsheetRect = useSpreadsheetRect();
    bubbleRef = useRef("bubble");
    setup() {
        useEffect(() => {
            const el = this.bubbleRef.el;
            if (!el) {
                return;
            }
            const anchorRect = this.props.anchorRect;
            const rect = getBoundingRectAsPOJO(el);
            const x = anchorRect.x + anchorRect.width / 2 - rect.width / 2 - this.spreadsheetRect.x;
            const y = anchorRect.y - rect.height - BUBBLE_ARROW_SIZE - this.spreadsheetRect.y;
            el.style.left = `${x}px`;
            el.style.top = `${y}px`;
        });
    }
}

const functions = functionRegistry.content;
const ASSISTANT_WIDTH = 300;
const CLOSE_ICON_RADIUS = 9;
const selectionIndicatorClass = "selector-flag";
const highlightParenthesisClass = "highlight-parenthesis-flag";
const highlightClass = "highlight-flag";
const selectionIndicatorColor = "#a9a9a9";
const selectionIndicator = "␣";
css /* scss */ `
  .o-composer-container {
    .o-composer {
      overflow-y: auto;
      overflow-x: hidden;
      word-break: break-all;
      padding-right: 2px;

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

      tab-size: 4;

      p {
        margin-bottom: 0px;

        span {
          white-space: pre-wrap;
          /* On some browsers (chromium ?) it is somehow possible to hover two of the composer's spans at the same time, leading to
           * flickering with a succession of onmouseenter/onmouseleave events. A small invisible padding seems to prevent the issue.*/
          padding-left: 0.01px;

          &.${selectionIndicatorClass}:after {
            content: "${selectionIndicator}";
            color: ${selectionIndicatorColor};
          }

          &.${highlightParenthesisClass}:not(.${highlightClass}) {
            border-radius: 5px;
            background-color: lightgray;
            padding: 1.5px 0px 1.5px 0px;
          }

          &.${highlightClass} {
            background-color: #e6edf3;
          }
        }
      }
    }
    .o-composer[placeholder]:empty:not(:focus):not(.active)::before {
      content: attr(placeholder);
      color: #bdbdbd;
      position: relative;
      top: 0%;
      pointer-events: none;
    }

    .fa-stack {
      /* reset stack size which is doubled by default */
      width: ${CLOSE_ICON_RADIUS * 2}px;
      height: ${CLOSE_ICON_RADIUS * 2}px;
      line-height: ${CLOSE_ICON_RADIUS * 2}px;
    }

    .force-open-assistant {
      left: -1px;
      top: -1px;

      .fa-question-circle {
        color: ${PRIMARY_BUTTON_BG};
      }
    }

    .o-composer-assistant {
      margin-top: 1px;

      .o-semi-bold {
        /* FIXME: to remove in favor of Bootstrap
        * 'fw-semibold' when we upgrade to Bootstrap 5.2
        */
        font-weight: 600 !important;
      }
    }
  }
`;
class Composer extends Component {
    static template = "o-spreadsheet-Composer";
    static props = {
        focus: {
            validate: (value) => ["inactive", "cellFocus", "contentFocus"].includes(value),
        },
        inputStyle: { type: String, optional: true },
        rect: { type: Object, optional: true },
        delimitation: { type: Object, optional: true },
        onComposerCellFocused: { type: Function, optional: true },
        onComposerContentFocused: Function,
        isDefaultFocus: { type: Boolean, optional: true },
        onInputContextMenu: { type: Function, optional: true },
        composerStore: Object,
        placeholder: { type: String, optional: true },
        inputMode: { type: String, optional: true },
        showAssistant: { type: Boolean, optional: true },
    };
    static components = { TextValueProvider, FunctionDescriptionProvider, SpeechBubble };
    static defaultProps = {
        inputStyle: "",
        isDefaultFocus: false,
        inputMode: "text",
        showAssistant: true,
    };
    DOMFocusableElementStore;
    composerRef = useRef("o_composer");
    contentHelper = new ContentEditableHelper(this.composerRef.el);
    composerState = useState({
        positionStart: 0,
        positionEnd: 0,
        hoveredRect: undefined,
    });
    functionDescriptionState = useState({
        showDescription: false,
        functionDescription: {},
        argsToFocus: [],
    });
    assistant = useState({
        forcedClosed: false,
    });
    compositionActive = false;
    spreadsheetRect = useSpreadsheetRect();
    lastHoveredTokenIndex = undefined;
    debouncedHover = debounce((tokenIndex, hoveredRect) => {
        const selection = this.contentHelper.getCurrentSelection();
        if (selection.start !== selection.end) {
            return;
        }
        const currentHoveredContext = this.props.composerStore.hoveredTokens;
        this.props.composerStore.hoverToken(tokenIndex);
        if (!deepEquals(currentHoveredContext, this.props.composerStore.hoveredTokens)) {
            this.composerState.hoveredRect = hoveredRect;
        }
    }, 120);
    get assistantStyleProperties() {
        const composerRect = this.composerRef.el.getBoundingClientRect();
        const assistantStyle = {};
        const minWidth = Math.min(this.props.rect?.width || Infinity, ASSISTANT_WIDTH);
        assistantStyle["min-width"] = `${minWidth}px`;
        const proposals = this.props.composerStore.autoCompleteProposals;
        const proposalsHaveDescription = proposals?.some((proposal) => proposal.description);
        if (this.functionDescriptionState.showDescription || proposalsHaveDescription) {
            assistantStyle.width = `${ASSISTANT_WIDTH}px`;
        }
        if (this.props.delimitation && this.props.rect) {
            const { x: cellX, y: cellY, height: cellHeight } = this.props.rect;
            const remainingHeight = this.props.delimitation.height - (cellY + cellHeight);
            assistantStyle["max-height"] = `${remainingHeight}px`;
            if (cellY > remainingHeight) {
                const availableSpaceAbove = cellY;
                assistantStyle["max-height"] = `${availableSpaceAbove - CLOSE_ICON_RADIUS}px`;
                // render top
                // We compensate 2 px of margin on the assistant style + 1px for design reasons
                assistantStyle.top = `-3px`;
                assistantStyle.transform = `translate(0, -100%)`;
            }
            if (cellX + ASSISTANT_WIDTH > this.props.delimitation.width) {
                // render left
                assistantStyle.right = `0px`;
            }
        }
        else {
            assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom - 1}px`; // -1: margin
            if (composerRect.left + ASSISTANT_WIDTH + SCROLLBAR_WIDTH + CLOSE_ICON_RADIUS >
                this.spreadsheetRect.width) {
                assistantStyle.right = `${CLOSE_ICON_RADIUS}px`;
            }
        }
        return assistantStyle;
    }
    get assistantStyle() {
        const allProperties = this.assistantStyleProperties;
        return cssPropertiesToCss({
            "max-height": allProperties["max-height"],
            width: allProperties["width"],
            "min-width": allProperties["min-width"],
        });
    }
    get assistantContainerStyle() {
        const allProperties = this.assistantStyleProperties;
        return cssPropertiesToCss({
            top: allProperties["top"],
            right: allProperties["right"],
            transform: allProperties["transform"],
        });
    }
    // we can't allow input events to be triggered while we remove and add back the content of the composer in processContent
    shouldProcessInputEvents = false;
    tokens = [];
    keyMapping = {
        Enter: (ev) => this.processEnterKey(ev, "down"),
        "Shift+Enter": (ev) => this.processEnterKey(ev, "up"),
        "Alt+Enter": this.processNewLineEvent,
        "Ctrl+Enter": this.processNewLineEvent,
        Escape: this.processEscapeKey,
        F2: (ev) => this.toggleEditionMode(ev),
        F4: (ev) => this.processF4Key(ev),
        Tab: (ev) => this.processTabKey(ev, "right"),
        "Shift+Tab": (ev) => this.processTabKey(ev, "left"),
    };
    keyCodeMapping = {
        NumpadDecimal: this.processNumpadDecimal,
    };
    setup() {
        this.DOMFocusableElementStore = useStore(DOMFocusableElementStore);
        onMounted(() => {
            const el = this.composerRef.el;
            if (this.props.isDefaultFocus) {
                this.DOMFocusableElementStore.setFocusableElement(el);
            }
            this.contentHelper.updateEl(el);
        });
        onWillUnmount(() => {
            this.debouncedHover.stopDebounce();
        });
        useEffect(() => {
            this.processContent();
            if (document.activeElement === this.contentHelper.el &&
                this.props.composerStore.editionMode === "inactive" &&
                !this.props.isDefaultFocus) {
                this.DOMFocusableElementStore.focus();
            }
        });
        useEffect(() => {
            this.processTokenAtCursor();
        }, () => [this.props.composerStore.editionMode !== "inactive"]);
        useEffect(() => {
            this.contentHelper.scrollSelectionIntoView();
        }, () => [
            this.props.composerStore.composerSelection.start,
            this.props.composerStore.composerSelection.end,
        ]);
    }
    // ---------------------------------------------------------------------------
    // Handlers
    // ---------------------------------------------------------------------------
    processArrowKeys(ev) {
        const tokenAtCursor = this.props.composerStore.tokenAtCursor;
        if ((this.props.composerStore.isSelectingRange ||
            this.props.composerStore.editionMode === "inactive") &&
            !(["ArrowUp", "ArrowDown"].includes(ev.key) &&
                this.props.composerStore.isAutoCompleteDisplayed &&
                tokenAtCursor?.type !== "REFERENCE")) {
            this.functionDescriptionState.showDescription = false;
            this.props.composerStore.hideHelp();
            // Prevent the default content editable behavior which moves the cursor
            ev.preventDefault();
            ev.stopPropagation();
            updateSelectionWithArrowKeys(ev, this.env.model.selection);
            return;
        }
        const content = this.props.composerStore.currentContent;
        if (this.props.focus === "cellFocus" &&
            !this.props.composerStore.isAutoCompleteDisplayed &&
            !isFormula(content)) {
            this.props.composerStore.stopEdition();
            return;
        }
        // All arrow keys are processed: up and down should move autocomplete, left
        // and right should move the cursor.
        ev.stopPropagation();
        this.handleArrowKeysForAutocomplete(ev);
    }
    handleArrowKeysForAutocomplete(ev) {
        // only for arrow up and down
        if (["ArrowUp", "ArrowDown"].includes(ev.key) &&
            this.props.composerStore.isAutoCompleteDisplayed) {
            ev.preventDefault();
            this.props.composerStore.moveAutoCompleteSelection(ev.key === "ArrowDown" ? "next" : "previous");
        }
    }
    processTabKey(ev, direction) {
        ev.preventDefault();
        ev.stopPropagation();
        this.props.composerStore.autoCompleteOrStop(direction, this.assistant.forcedClosed);
    }
    processEnterKey(ev, direction) {
        ev.preventDefault();
        ev.stopPropagation();
        this.props.composerStore.autoCompleteOrStop(direction, this.assistant.forcedClosed);
    }
    processNewLineEvent(ev) {
        ev.preventDefault();
        ev.stopPropagation();
        const content = this.contentHelper.getText();
        const selection = this.contentHelper.getCurrentSelection();
        const start = Math.min(selection.start, selection.end);
        const end = Math.max(selection.start, selection.end);
        this.props.composerStore.stopComposerRangeSelection();
        this.props.composerStore.setCurrentContent(content.slice(0, start) + NEWLINE + content.slice(end), {
            start: start + 1,
            end: start + 1,
        });
        this.processContent();
    }
    processEscapeKey(ev) {
        this.props.composerStore.cancelEdition();
        ev.stopPropagation();
        ev.preventDefault();
    }
    processF4Key(ev) {
        ev.stopPropagation();
        this.props.composerStore.cycleReferences();
        this.processContent();
    }
    toggleEditionMode(ev) {
        ev.stopPropagation();
        this.props.composerStore.toggleEditionMode();
        this.processContent();
    }
    processNumpadDecimal(ev) {
        ev.stopPropagation();
        ev.preventDefault();
        const locale = this.env.model.getters.getLocale();
        const selection = this.contentHelper.getCurrentSelection();
        const currentContent = this.props.composerStore.currentContent;
        const content = currentContent.slice(0, selection.start) +
            locale.decimalSeparator +
            currentContent.slice(selection.end);
        // Update composer even by hand rather than dispatching an InputEvent because untrusted inputs
        // events aren't handled natively by contentEditable
        this.props.composerStore.setCurrentContent(content, {
            start: selection.start + 1,
            end: selection.start + 1,
        });
        // We need to do the process content here in case there is no render between the keyDown and the
        // keyUp event
        this.processContent();
    }
    onCompositionStart() {
        this.compositionActive = true;
    }
    onCompositionEnd() {
        this.compositionActive = false;
    }
    onKeydown(ev) {
        if (this.props.composerStore.editionMode === "inactive") {
            return;
        }
        if (ev.key.startsWith("Arrow")) {
            this.processArrowKeys(ev);
            return;
        }
        const handler = this.keyMapping[keyboardEventToShortcutString(ev)] ||
            this.keyCodeMapping[keyboardEventToShortcutString(ev, "code")];
        if (handler) {
            handler.call(this, ev);
        }
        else {
            ev.stopPropagation();
        }
    }
    onPaste(ev) {
        if (this.props.composerStore.editionMode !== "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) {
        if (!this.shouldProcessInputEvents) {
            return;
        }
        ev.stopPropagation();
        let content;
        if (this.props.composerStore.editionMode === "inactive") {
            content = ev.data || "";
        }
        else {
            content = this.contentHelper.getText();
        }
        if (this.props.focus === "inactive") {
            return this.props.onComposerCellFocused?.(content);
        }
        const selection = this.contentHelper.getCurrentSelection();
        this.props.composerStore.stopComposerRangeSelection();
        this.props.composerStore.setCurrentContent(content, selection);
        this.processTokenAtCursor();
    }
    onKeyup(ev) {
        if (this.contentHelper.el === document.activeElement) {
            if (this.props.composerStore.isAutoCompleteDisplayed &&
                ["ArrowUp", "ArrowDown"].includes(ev.key)) {
                return;
            }
            if (this.props.composerStore.isSelectingRange && ev.key?.startsWith("Arrow")) {
                return;
            }
            const { start: oldStart, end: oldEnd } = this.props.composerStore.composerSelection;
            const { start, end } = this.contentHelper.getCurrentSelection();
            if (start !== oldStart || end !== oldEnd) {
                this.props.composerStore.changeComposerCursorSelection(start, end);
            }
            this.processTokenAtCursor();
        }
    }
    onBlur(ev) {
        if (this.props.composerStore.editionMode === "inactive") {
            return;
        }
        const target = ev.relatedTarget;
        if (!target || !(target instanceof HTMLElement)) {
            this.props.composerStore.stopEdition();
            return;
        }
        if (target.attributes.getNamedItem("composerFocusableElement")) {
            this.contentHelper.el.focus();
            return;
        }
        if (target.classList.contains("o-composer")) {
            return;
        }
        this.props.composerStore.stopEdition();
    }
    /**
     * This is required to ensure the content helper selection is
     * properly updated on "onclick" events. Depending on the browser,
     * the callback onClick from the composer will be executed before
     * the selection was updated in the dom, which means we capture an
     * wrong selection which is then forced upon the content helper on
     * processContent.
     */
    onMousedown(ev) {
        if (ev.button > 0) {
            // not main button, probably a context menu
            return;
        }
        if (this.env.isMobile() && !isIOS()) {
            return;
        }
        this.debouncedHover.stopDebounce();
        this.contentHelper.removeSelection();
    }
    onMouseup() {
        if (this.env.model.getters.isReadonly()) {
            return;
        }
        const selection = this.contentHelper.getCurrentSelection();
        if (selection.start !== selection.end) {
            this.props.composerStore.hoverToken(undefined);
        }
    }
    onClick() {
        if (this.env.model.getters.isReadonly()) {
            return;
        }
        const newSelection = this.contentHelper.getCurrentSelection();
        const isCurrentlyInactive = this.props.composerStore.editionMode === "inactive";
        this.props.onComposerContentFocused(newSelection);
        if (!isCurrentlyInactive) {
            this.props.composerStore.changeComposerCursorSelection(newSelection.start, newSelection.end);
        }
        this.processTokenAtCursor();
    }
    onDblClick() {
        if (this.env.model.getters.isReadonly()) {
            return;
        }
        const composerContent = this.props.composerStore.currentContent;
        const isValidFormula = isFormula(composerContent);
        if (isValidFormula) {
            const tokens = this.props.composerStore.currentTokens;
            const currentSelection = this.contentHelper.getCurrentSelection();
            if (currentSelection.start === currentSelection.end)
                return;
            const currentSelectedText = composerContent.substring(currentSelection.start, currentSelection.end);
            const token = tokens.filter((token) => token.value.includes(currentSelectedText) &&
                token.start <= currentSelection.start &&
                token.end >= currentSelection.end)[0];
            if (!token) {
                return;
            }
            if (token.type === "REFERENCE") {
                this.props.composerStore.changeComposerCursorSelection(token.start, token.end);
            }
        }
    }
    onContextMenu(ev) {
        if (this.props.composerStore.editionMode === "inactive") {
            this.props.onInputContextMenu?.(ev);
        }
    }
    closeAssistant() {
        if (!this.props.composerStore.canBeToggled)
            return;
        this.assistant.forcedClosed = true;
    }
    openAssistant() {
        if (!this.props.composerStore.canBeToggled)
            return;
        this.assistant.forcedClosed = false;
    }
    onWheel(event) {
        // detect if scrollbar is available
        if (this.composerRef.el &&
            this.composerRef.el.scrollHeight > this.composerRef.el.clientHeight) {
            event.stopPropagation();
        }
    }
    // ---------------------------------------------------------------------------
    // Private
    // ---------------------------------------------------------------------------
    processContent() {
        if (this.compositionActive) {
            return;
        }
        this.shouldProcessInputEvents = false;
        if (this.props.focus !== "inactive" && document.activeElement !== this.contentHelper.el) {
            this.contentHelper.el.focus();
        }
        const content = this.getContentLines();
        this.contentHelper.setText(content);
        if (content.length !== 0 && content.length[0] !== 0) {
            if (this.props.focus !== "inactive") {
                // Put the cursor back where it was before the rendering
                const { start, end } = this.props.composerStore.composerSelection;
                this.contentHelper.selectRange(start, end);
            }
        }
        this.shouldProcessInputEvents = true;
    }
    /**
     * Get the HTML content corresponding to the current composer token, divided by lines.
     */
    getContentLines() {
        const value = this.props.composerStore.currentContent;
        const isValidFormula = isFormula(value);
        if (value === "") {
            return [];
        }
        else if (isValidFormula && this.props.focus !== "inactive") {
            return this.splitHtmlContentIntoLines(this.getHtmlContentFromTokens());
        }
        return this.splitHtmlContentIntoLines([{ value, classes: [] }]);
    }
    getHtmlContentFromTokens() {
        const tokens = this.props.composerStore.currentTokens;
        const result = [];
        const { end, start } = this.props.composerStore.composerSelection;
        for (let index = 0; index < tokens.length; index++) {
            const token = tokens[index];
            let color = token.color || DEFAULT_TOKEN_COLOR;
            if (token.isBlurred) {
                color = setColorAlpha(color, 0.5);
            }
            const classes = [];
            if (token.type === "REFERENCE" &&
                this.props.composerStore.tokenAtCursor === token &&
                this.props.composerStore.editionMode === "selecting") {
                classes.push("text-decoration-underline");
            }
            if (end === start && token.isParenthesisLinkedToCursor) {
                classes.push(highlightParenthesisClass);
            }
            if (token.isInHoverContext) {
                classes.push(highlightClass);
            }
            if (this.props.composerStore.showSelectionIndicator && end === start && end === token.end) {
                classes.push(selectionIndicatorClass);
            }
            result.push({
                value: token.value,
                color,
                classes,
                onHover: (rect) => this.onTokenHover(index, rect),
                onStopHover: () => this.onTokenHover(undefined),
            });
        }
        return result;
    }
    onTokenHover(tokenIndex, hoveredRect) {
        // We want to debounce the hover event to avoid flickering, but we also don't want to keep delaying the debounce timer
        // if the user keeps moving its mouse over the same token.
        if (this.lastHoveredTokenIndex !== tokenIndex) {
            this.lastHoveredTokenIndex = tokenIndex;
            this.debouncedHover(tokenIndex, hoveredRect);
        }
    }
    /**
     * Split an array of HTMLContents into lines. Each NEWLINE character encountered will create a new
     * line. Contents can be split into multiple parts if they contain multiple NEWLINE characters.
     */
    splitHtmlContentIntoLines(contents) {
        const contentSplitInLines = [];
        let currentLine = [];
        for (const content of contents) {
            if (content.value.includes(NEWLINE)) {
                const lines = content.value.split(NEWLINE);
                const lastLine = lines.pop();
                for (const line of lines) {
                    currentLine.push({ color: content.color, value: line }); // don't copy class, only last line should keep it
                    contentSplitInLines.push(currentLine);
                    currentLine = [];
                }
                currentLine.push({ ...content, value: lastLine });
            }
            else {
                currentLine.push(content);
            }
        }
        if (currentLine.length) {
            contentSplitInLines.push(currentLine);
        }
        // Remove useless empty contents
        const filteredLines = [];
        for (const line of contentSplitInLines) {
            if (line.every(this.isContentEmpty)) {
                filteredLines.push([line[0]]);
            }
            else {
                filteredLines.push(line.filter((content) => !this.isContentEmpty(content)));
            }
        }
        return filteredLines;
    }
    isContentEmpty(content) {
        return !(content.value || content.classes?.length);
    }
    /**
     * 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() {
        const composerStore = this.props.composerStore;
        this.functionDescriptionState.showDescription = false;
        const token = this.props.composerStore.tokenAtCursor;
        if (isFormula(composerStore.currentContent) && token && token.type !== "SYMBOL") {
            const tokenContext = token.functionContext;
            const parentFunction = tokenContext?.parent.toUpperCase();
            if (tokenContext &&
                parentFunction &&
                parentFunction in functions &&
                token.type !== "UNKNOWN") {
                // initialize Formula Assistant
                const description = functions[parentFunction];
                const argPosition = tokenContext.argPosition;
                const nbrArgSupplied = tokenContext.args.length;
                this.functionDescriptionState.functionDescription = description;
                const isParenthesisClosed = this.props.composerStore.currentTokens.some((t) => t.type === "RIGHT_PAREN" && t.parenthesesCode === token.parenthesesCode);
                this.functionDescriptionState.argsToFocus = this.getArgsToFocus(isParenthesisClosed, description, nbrArgSupplied, argPosition);
                this.functionDescriptionState.showDescription = true;
            }
        }
    }
    /**
     * Compute the arguments to focus depending on the current value position.
     *
     * Normally, 'argTargeting' is used to compute the argument to focus, but in the composer,
     * we don't yet know how many arguments the user will supply.
     *
     * This function computes all the possible arguments to focus for different numbers of arguments supplied.
     */
    getArgsToFocus(isParenthesisClosed, description, nbrArgSupplied, argPosition) {
        const { nbrArgRepeating, minArgRequired, nbrArgOptional, maxArgPossible } = description;
        // When the parenthesis is closed, we consider the user is done with the function,
        // so we know exactly the number of arguments supplied.
        if (isParenthesisClosed) {
            const focusedArg = argTargeting(description, Math.max(Math.min(maxArgPossible, nbrArgSupplied), minArgRequired))(argPosition);
            return focusedArg !== undefined ? [focusedArg] : [];
        }
        // Otherwise, the user is still typing the formula, so we don't know yet how many arguments the user will supply.
        // Consequently, we need to compute for all possible numbers of arguments supplied.
        const minArgsNumberPossibility = Math.max(nbrArgSupplied, minArgRequired);
        const maxArgsNumberPossibility = nbrArgRepeating
            ? minArgRequired +
                Math.ceil((minArgsNumberPossibility - minArgRequired) / nbrArgRepeating) * nbrArgRepeating +
                nbrArgOptional
            : maxArgPossible;
        const argsToFocus = [];
        for (let i = minArgsNumberPossibility; i <= maxArgsNumberPossibility; i++) {
            const focusedArg = argTargeting(description, i)(argPosition);
            if (focusedArg !== undefined) {
                argsToFocus.push(focusedArg);
            }
        }
        return [...new Set(argsToFocus)];
    }
    autoComplete(value) {
        if (!value || (this.assistant.forcedClosed && this.props.composerStore.canBeToggled)) {
            return;
        }
        this.props.composerStore.insertAutoCompleteValue(value);
        this.processTokenAtCursor();
    }
    get displaySpeechBubble() {
        return !!(this.props.focus !== "inactive" &&
            this.composerState.hoveredRect &&
            this.props.composerStore.hoveredContentEvaluation);
    }
}

/**
 * KeepLast is a concurrency primitive that manages a list of tasks, and only
 * keeps the last task active.
 *
 */
class KeepLast {
    lastId = 0;
    /**
     * Register a new task
     */
    add(promise) {
        this.lastId++;
        const currentId = this.lastId;
        return new Promise((resolve, reject) => {
            promise
                .then((value) => {
                if (this.lastId === currentId) {
                    resolve(value);
                }
            })
                .catch((reason) => {
                if (this.lastId === currentId) {
                    reject(reason);
                }
            });
        });
    }
}

function drawHighlight(renderingContext, highlight, rect) {
    const { x, y, width, height } = rect;
    if (width < 0 || height < 0) {
        return;
    }
    const color = highlight.color || HIGHLIGHT_COLOR;
    const { ctx } = renderingContext;
    ctx.save();
    if (!highlight.noBorder) {
        if (highlight.dashed) {
            ctx.setLineDash([5, 3]);
        }
        ctx.strokeStyle = color;
        if (highlight.thinLine) {
            ctx.lineWidth = 1;
            ctx.strokeRect(x, y, width, height);
        }
        else {
            ctx.lineWidth = 2;
            /** + 0.5 offset to have sharp lines. See comment in {@link RendererPlugin#drawBorder} for more details */
            ctx.strokeRect(x + 0.5, y + 0.5, width, height);
        }
    }
    if (!highlight.noFill) {
        ctx.fillStyle = setColorAlpha(toHex(color), highlight.fillAlpha ?? 0.12);
        ctx.fillRect(x, y, width, height);
    }
    ctx.restore();
}

class HighlightStore extends SpreadsheetStore {
    mutators = ["register", "unRegister"];
    providers = [];
    constructor(get) {
        super(get);
        this.onDispose(() => {
            this.providers = [];
        });
    }
    get renderingLayers() {
        return ["Highlights"];
    }
    get highlights() {
        const activeSheetId = this.getters.getActiveSheetId();
        return this.providers
            .flatMap((h) => h.highlights)
            .filter((h) => h.range.sheetId === activeSheetId)
            .map((highlight) => {
            const { numberOfRows, numberOfCols } = zoneToDimension(highlight.range.zone);
            const zone = numberOfRows * numberOfCols === 1
                ? this.getters.expandZone(highlight.range.sheetId, highlight.range.zone)
                : highlight.range.unboundedZone;
            return {
                ...highlight,
                range: this.model.getters.getRangeFromZone(highlight.range.sheetId, zone),
            };
        });
    }
    register(highlightProvider) {
        this.providers.push(highlightProvider);
    }
    unRegister(highlightProvider) {
        this.providers = this.providers.filter((h) => h !== highlightProvider);
    }
    drawLayer(ctx, layer) {
        if (layer === "Highlights") {
            for (const highlight of this.highlights) {
                const rect = this.getters.getVisibleRect(highlight.range.zone);
                drawHighlight(ctx, highlight, rect);
            }
        }
    }
}

class NotificationStore {
    mutators = [
        "notifyUser",
        "raiseError",
        "askConfirmation",
        "updateNotificationCallbacks",
    ];
    notifyUser = (notification) => window.alert(notification.text);
    askConfirmation = (content, confirm, cancel) => {
        if (window.confirm(content)) {
            confirm();
        }
        else {
            cancel?.();
        }
    };
    raiseError = (text, callback) => {
        window.alert(text);
        callback?.();
    };
    updateNotificationCallbacks(methods) {
        this.notifyUser = methods.notifyUser || this.notifyUser;
        this.raiseError = methods.raiseError || this.raiseError;
        this.askConfirmation = methods.askConfirmation || this.askConfirmation;
    }
}

class AutoCompleteStore extends SpreadsheetStore {
    mutators = ["useProvider", "moveSelection", "hide", "selectIndex"];
    selectedIndex = undefined;
    provider;
    get selectedProposal() {
        if (this.selectedIndex === undefined || this.provider === undefined) {
            return undefined;
        }
        return this.provider.proposals[this.selectedIndex];
    }
    useProvider(provider) {
        this.provider = provider;
        this.selectedIndex = provider.autoSelectFirstProposal ? 0 : undefined;
    }
    hide() {
        this.provider = undefined;
        this.selectedIndex = undefined;
    }
    selectIndex(index) {
        this.selectedIndex = index;
    }
    moveSelection(direction) {
        if (!this.provider) {
            return;
        }
        if (this.selectedIndex === undefined) {
            this.selectedIndex = 0;
            return;
        }
        if (direction === "previous") {
            this.selectedIndex--;
            if (this.selectedIndex < 0) {
                this.selectedIndex = this.provider.proposals.length - 1;
            }
        }
        else {
            this.selectedIndex = (this.selectedIndex + 1) % this.provider.proposals.length;
        }
    }
}

class AbstractComposerStore extends SpreadsheetStore {
    mutators = [
        "startEdition",
        "setCurrentContent",
        "stopEdition",
        "stopComposerRangeSelection",
        "cancelEdition",
        "cycleReferences",
        "hideHelp",
        "autoCompleteOrStop",
        "insertAutoCompleteValue",
        "moveAutoCompleteSelection",
        "selectAutoCompleteIndex",
        "toggleEditionMode",
        "changeComposerCursorSelection",
        "replaceComposerCursorSelection",
        "hoverToken",
    ];
    col = 0;
    row = 0;
    editionMode = "inactive";
    sheetId = "";
    _currentContent = "";
    currentTokens = [];
    selectionStart = 0;
    selectionEnd = 0;
    initialContent = "";
    colorIndexByRange = {};
    autoComplete = new AutoCompleteStore(this.get);
    hoveredTokens = [];
    hoveredContentEvaluation = "";
    autoCompleteKeepLast = new KeepLast();
    notificationStore = this.get(NotificationStore);
    highlightStore = this.get(HighlightStore);
    constructor(get) {
        super(get);
        this.highlightStore.register(this);
        this.onDispose(() => {
            this.highlightStore.unRegister(this);
            this._cancelEdition();
        });
    }
    handleEvent(event) {
        const sheetId = this.getters.getActiveSheetId();
        let unboundedZone;
        if (event.options.unbounded) {
            unboundedZone = this.getters.getUnboundedZone(sheetId, event.anchor.zone);
        }
        else {
            unboundedZone = event.anchor.zone;
        }
        switch (event.mode) {
            case "newAnchor":
                if (this.editionMode === "selecting") {
                    this.insertSelectedRange(unboundedZone);
                }
                break;
            default:
                if (this.editionMode === "selecting") {
                    this.replaceSelectedRange(unboundedZone);
                }
                else {
                    this.updateComposerRange(event.previousAnchor.zone, unboundedZone);
                }
                break;
        }
    }
    changeComposerCursorSelection(start, end) {
        if (!this.isSelectionValid(this._currentContent.length, start, end)) {
            return;
        }
        this.selectionStart = start;
        this.selectionEnd = end;
        this.stopComposerRangeSelection();
        this.computeFormulaCursorContext();
        this.computeParenthesisRelatedToCursor();
        this.updateAutoCompleteProvider();
    }
    stopComposerRangeSelection() {
        if (this.isSelectingRange) {
            this.editionMode = "editing";
        }
    }
    startEdition(text, selection) {
        const { col, row } = this.getters.getActivePosition();
        this.model.dispatch("SELECT_FIGURE", { figureId: null });
        this.model.dispatch("SCROLL_TO_CELL", { col, row });
        if (this.editionMode !== "inactive" && text) {
            this.setContent(text, selection);
        }
        else {
            this._startEdition(text, selection);
        }
        this.updateTokenColor();
        this.computeFormulaCursorContext();
        this.computeParenthesisRelatedToCursor();
        this.updateAutoCompleteProvider();
    }
    cancelEdition() {
        this.resetContent();
        this.cancelEditionAndActivateSheet();
    }
    setCurrentContent(content, selection) {
        if (selection && !this.isSelectionValid(content.length, selection.start, selection.end)) {
            return;
        }
        this.setContent(content, selection, true);
        this.updateTokenColor();
        this.computeFormulaCursorContext();
        this.computeParenthesisRelatedToCursor();
    }
    replaceComposerCursorSelection(text) {
        this.replaceSelection(text);
    }
    handle(cmd) {
        switch (cmd.type) {
            case "SELECT_FIGURE":
                if (cmd.figureId) {
                    this.resetContent();
                    this.cancelEditionAndActivateSheet();
                }
                break;
            case "START_CHANGE_HIGHLIGHT":
                const { left, top } = cmd.zone;
                // changing the highlight can conflit with the 'selecting' mode
                if (this.isSelectingRange) {
                    this.editionMode = "editing";
                }
                this.model.selection.resetAnchor(this, {
                    cell: { col: left, row: top },
                    zone: cmd.zone,
                });
                break;
        }
    }
    // ---------------------------------------------------------------------------
    // Getters
    // ---------------------------------------------------------------------------
    get currentContent() {
        if (this.editionMode === "inactive") {
            return this.getComposerContent(this.getters.getActivePosition()).text;
        }
        return this._currentContent;
    }
    get composerSelection() {
        return {
            start: this.selectionStart,
            end: this.selectionEnd,
        };
    }
    get isSelectingRange() {
        return this.editionMode === "selecting";
    }
    get showSelectionIndicator() {
        return this.isSelectingRange && this.canStartComposerRangeSelection();
    }
    /**
     * Return the (enriched) token just before the cursor.
     */
    get tokenAtCursor() {
        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);
        }
    }
    get autoCompleteProposals() {
        return this.autoComplete.provider?.proposals || [];
    }
    get autoCompleteSelectedIndex() {
        return this.autoComplete.selectedIndex;
    }
    get isAutoCompleteDisplayed() {
        return !!this.autoComplete.provider;
    }
    get canBeToggled() {
        return this.autoComplete.provider?.canBeToggled ?? true;
    }
    cycleReferences() {
        const locale = this.getters.getLocale();
        const updated = cycleFixedReference(this.composerSelection, this._currentContent, locale);
        if (updated === undefined) {
            return;
        }
        this.setCurrentContent(updated.content, updated.selection);
    }
    toggleEditionMode() {
        if (this.editionMode === "inactive")
            return;
        const start = Math.min(this.selectionStart, this.selectionEnd);
        const end = Math.max(this.selectionStart, this.selectionEnd);
        const refToken = [...this.currentTokens]
            .reverse()
            .find((tk) => tk.end >= start && end >= tk.start && tk.type === "REFERENCE");
        if (this.editionMode === "editing" && refToken) {
            const currentSheetId = this.getters.getActiveSheetId();
            const { sheetName, xc } = splitReference(refToken.value);
            const sheetId = this.getters.getSheetIdByName(sheetName);
            if (sheetId && sheetId !== currentSheetId) {
                this.model.dispatch("ACTIVATE_SHEET", { sheetIdFrom: currentSheetId, sheetIdTo: sheetId });
            }
            // move cursor to the right part of the token
            this.selectionStart = this.selectionEnd = refToken.end;
            const zone = this.getters.getRangeFromSheetXC(this.sheetId, xc).zone;
            this.captureSelection(zone);
            this.editionMode = "selecting";
        }
        else {
            this.editionMode = "editing";
        }
    }
    hoverToken(tokenIndex) {
        this.currentTokens.forEach((t) => (t.isInHoverContext = undefined));
        const tokens = [...this.currentTokens];
        if (tokenIndex === undefined || ["ARG_SEPARATOR", "SPACE"].includes(tokens[tokenIndex].type)) {
            this.hoveredContentEvaluation = "";
            this.hoveredTokens = [];
            return;
        }
        const missingParenthesis = this.getNumberOfMissingParenthesis(tokens);
        if (missingParenthesis > 0) {
            tokens.push(...Array(missingParenthesis).fill({ value: ")", type: "RIGHT_PAREN" }));
        }
        let hoveredContextTokens = tokens;
        if (tokenIndex !== 0) {
            const relatedTokens = this.getRelatedTokens(tokens, tokenIndex);
            const notHoveredTokens = tokens.filter((t) => !relatedTokens.includes(t) && t.type !== "SPACE");
            // Includes starting "=" if all the other tokens are hovered
            hoveredContextTokens =
                notHoveredTokens.length === 1 && notHoveredTokens[0] === tokens[0] ? tokens : relatedTokens;
        }
        hoveredContextTokens.forEach((t) => (t.isInHoverContext = true));
        let hoveredFormula = hoveredContextTokens.map((t) => t.value).join("");
        if (!isFormula(hoveredFormula)) {
            hoveredFormula = `=${hoveredFormula}`;
        }
        const canonicalFormula = canonicalizeNumberContent(hoveredFormula, this.getters.getLocale());
        const result = this.evaluateCanonicalFormula(canonicalFormula);
        this.hoveredTokens = hoveredContextTokens;
        this.hoveredContentEvaluation = this.evaluationResultToDisplayString(result);
    }
    evaluateCanonicalFormula(canonicalFormula) {
        return this.getters.evaluateFormulaResult(this.sheetId, canonicalFormula);
    }
    getRelatedTokens(tokens, tokenIndex) {
        try {
            const ast = parseTokens(tokens);
            let match = undefined;
            for (const node of iterateAstNodes(ast)) {
                if (tokenIndex >= node.tokenStartIndex && tokenIndex <= node.tokenEndIndex) {
                    match = node;
                }
                else if (tokenIndex < node.tokenStartIndex) {
                    break;
                }
            }
            if (!match) {
                return tokens; // Happens if we're hovering spaces at the start/end of the formula
            }
            return tokens.slice(match.tokenStartIndex, match.tokenEndIndex + 1);
        }
        catch (e) {
            if (e instanceof EvaluationError) {
                return tokens;
            }
            throw e;
        }
    }
    evaluationResultToDisplayString(result) {
        const locale = this.getters.getLocale();
        if (isMatrix(result)) {
            const rowSeparator = locale.decimalSeparator === "," ? "/" : ",";
            const arrayStr = transposeMatrix(result)
                .map((row) => row.map((val) => this.cellValueToDisplayString(val)).join(rowSeparator))
                .join(";");
            return `{${arrayStr}}`;
        }
        return this.cellValueToDisplayString(result);
    }
    cellValueToDisplayString(result) {
        const value = result.value;
        switch (typeof value) {
            case "number":
                return formatValue(value, { locale: this.getters.getLocale(), format: result.format });
            case "string":
                if (isEvaluationError(value)) {
                    return value;
                }
                return `"${value}"`;
            case "boolean":
                return value ? "TRUE" : "FALSE";
        }
        return "0";
    }
    captureSelection(zone, col, row) {
        this.model.selection.capture(this, {
            cell: { col: col ?? zone.left, row: row ?? zone.top },
            zone,
        }, {
            handleEvent: this.handleEvent.bind(this),
            release: () => {
                this._stopEdition();
            },
        });
    }
    isSelectionValid(length, start, end) {
        return start >= 0 && start <= length && end >= 0 && end <= length;
    }
    /**
     * Enable the selecting mode
     */
    startComposerRangeSelection() {
        if (this.sheetId === this.getters.getActiveSheetId()) {
            const zone = positionToZone({ col: this.col, row: this.row });
            this.model.selection.resetAnchor(this, {
                cell: { col: this.col, row: this.row },
                zone,
            });
        }
        this.editionMode = "selecting";
    }
    /**
     * start the edition of a cell
     * @param str the key that is used to start the edition if it is a "content" key like a letter or number
     * @param selection
     * @private
     */
    _startEdition(str, selection) {
        const evaluatedCell = this.getters.getActiveCell();
        const locale = this.getters.getLocale();
        if (str && evaluatedCell.format?.includes("%") && isNumber(str, locale)) {
            selection = selection || { start: str.length, end: str.length };
            str = `${str}%`;
        }
        const { col, row, sheetId } = this.getters.getActivePosition();
        this.col = col;
        this.sheetId = sheetId;
        this.row = row;
        this.editionMode = "editing";
        const { text, adjustedSelection } = this.getComposerContent({ sheetId, col, row }, selection);
        this.initialContent = text;
        this.setContent(str || this.initialContent, adjustedSelection ?? selection);
        this.colorIndexByRange = {};
        const zone = positionToZone({ col: this.col, row: this.row });
        this.captureSelection(zone, col, row);
    }
    _stopEdition() {
        if (this.editionMode !== "inactive") {
            this.cancelEditionAndActivateSheet();
            let content = this.getCurrentCanonicalContent();
            const didChange = this.initialContent !== content;
            if (!didChange) {
                return;
            }
            if (content) {
                if (isFormula(content)) {
                    const missing = this.getNumberOfMissingParenthesis(this.currentTokens);
                    if (missing > 0) {
                        content += concat$1(new Array(missing).fill(")"));
                    }
                }
            }
            this.confirmEdition(content);
        }
    }
    getCurrentCanonicalContent() {
        return canonicalizeNumberContent(this._currentContent, this.getters.getLocale());
    }
    cancelEditionAndActivateSheet() {
        if (this.editionMode === "inactive") {
            return;
        }
        this._cancelEdition();
        const sheetId = this.getters.getActiveSheetId();
        if (sheetId !== this.sheetId) {
            this.model.dispatch("ACTIVATE_SHEET", {
                sheetIdFrom: this.getters.getActiveSheetId(),
                sheetIdTo: this.sheetId,
            });
        }
    }
    _cancelEdition() {
        if (this.editionMode === "inactive") {
            return;
        }
        this.editionMode = "inactive";
        this.model.selection.release(this);
        this.colorIndexByRange = {};
        this.hoveredTokens = [];
        this.hoveredContentEvaluation = "";
        this.hideHelp();
    }
    /**
     * Reset the current content to the active cell content
     */
    resetContent() {
        this.setContent(this.initialContent || "");
    }
    setContent(text, selection, raise) {
        const isNewCurrentContent = this._currentContent !== text;
        this._currentContent = text;
        if (selection) {
            this.selectionStart = selection.start;
            this.selectionEnd = selection.end;
        }
        else {
            this.selectionStart = this.selectionEnd = text.length;
        }
        if (isNewCurrentContent || this.editionMode !== "inactive") {
            const locale = this.getters.getLocale();
            this.currentTokens = isFormula(text) ? composerTokenize(text, locale) : [];
            const nonSpaceTokensCount = this.currentTokens.filter((token) => token.type !== "SPACE").length;
            if (nonSpaceTokensCount > 1000) {
                if (raise) {
                    this.notificationStore.raiseError(_t("This formula has over 1000 parts. It can't be processed properly, consider splitting it into multiple cells"));
                }
            }
        }
        if (this.canStartComposerRangeSelection()) {
            this.startComposerRangeSelection();
        }
        this.updateAutoCompleteProvider();
    }
    getAutoCompleteProviders() {
        return autoCompleteProviders.getAll();
    }
    insertSelectedRange(zone) {
        // infer if range selected or selecting range from cursor position
        const start = Math.min(this.selectionStart, this.selectionEnd);
        const ref = this.getZoneReference(zone);
        if (this.canStartComposerRangeSelection()) {
            this.insertText(ref, start);
        }
        else {
            this.insertText("," + ref, start);
        }
    }
    /**
     * Replace the current reference selected by the new one.
     * */
    replaceSelectedRange(zone) {
        const ref = this.getZoneReference(zone);
        const currentToken = this.tokenAtCursor;
        const start = currentToken?.type === "REFERENCE" ? currentToken.start : this.selectionStart;
        this.replaceText(ref, start, this.selectionEnd);
    }
    /**
     * Replace the reference of the old zone by the new one.
     */
    updateComposerRange(oldZone, newZone) {
        const activeSheetId = this.getters.getActiveSheetId();
        const tokentAtCursor = this.tokenAtCursor;
        const tokens = tokentAtCursor ? [tokentAtCursor, ...this.currentTokens] : this.currentTokens;
        const previousRefToken = tokens
            .filter((token) => token.type === "REFERENCE")
            .find((token) => {
            const { xc, sheetName: sheet } = splitReference(token.value);
            const sheetName = sheet || this.getters.getSheetName(this.sheetId);
            if (!isSheetNameEqual(this.getters.getSheetName(activeSheetId), sheetName)) {
                return false;
            }
            const refRange = this.getters.getRangeFromSheetXC(activeSheetId, xc);
            return isEqual(this.getters.expandZone(activeSheetId, refRange.zone), oldZone);
        });
        if (!previousRefToken) {
            return;
        }
        const previousRange = this.getters.getRangeFromSheetXC(activeSheetId, previousRefToken.value);
        this.selectionStart = previousRefToken.start;
        this.selectionEnd = this.selectionStart + previousRefToken.value.length;
        const newRange = this.getters.getRangeFromZone(activeSheetId, newZone);
        const newRef = this.getRangeReference(newRange, previousRange.parts);
        this.replaceSelection(newRef);
    }
    getZoneReference(zone) {
        const inputSheetId = this.sheetId;
        const sheetId = this.getters.getActiveSheetId();
        const range = this.getters.getRangeFromZone(sheetId, zone);
        return this.getters.getSelectionRangeString(range, inputSheetId);
    }
    getRangeReference(range, fixedParts) {
        const _fixedParts = [...fixedParts];
        const newRange = { ...range, parts: _fixedParts };
        return this.getters.getSelectionRangeString(newRange, this.sheetId);
    }
    /**
     * Replace the current selection by a new text.
     * The cursor is then set at the end of the text.
     */
    replaceSelection(text) {
        const start = Math.min(this.selectionStart, this.selectionEnd);
        const end = Math.max(this.selectionStart, this.selectionEnd);
        this.replaceText(text, start, end);
    }
    replaceText(text, start, end) {
        this._currentContent =
            this._currentContent.slice(0, start) +
                this._currentContent.slice(end, this._currentContent.length);
        this.insertText(text, start);
    }
    /**
     * Insert a text at the given position.
     * The cursor is then set at the end of the text.
     */
    insertText(text, start) {
        const content = this._currentContent.slice(0, start) + text + this._currentContent.slice(start);
        const end = start + text.length;
        this.setCurrentContent(content, { start: end, end });
    }
    updateTokenColor() {
        this.updateRangeColor();
        for (let i = 0; i < this.currentTokens.length; i++) {
            this.currentTokens[i].color = this.getTokenColor(this.currentTokens[i]);
        }
    }
    getTokenColor(token) {
        if (token.type === "REFERENCE") {
            const { xc, sheetName } = splitReference(token.value);
            return this.rangeColor(xc, sheetName) || DEFAULT_TOKEN_COLOR;
        }
        if (token.type === "SYMBOL") {
            const upperCaseValue = token.value.toUpperCase();
            if (upperCaseValue === "TRUE" || upperCaseValue === "FALSE") {
                return tokenColors.NUMBER;
            }
            if (upperCaseValue in functionRegistry.content) {
                return tokenColors.FUNCTION;
            }
        }
        if (["LEFT_PAREN", "RIGHT_PAREN"].includes(token.type)) {
            if (token.parenthesesCode === "") {
                return tokenColors.ORPHAN_RIGHT_PAREN;
            }
        }
        return tokenColors[token.type] || DEFAULT_TOKEN_COLOR;
    }
    rangeColor(xc, sheetName) {
        const refSheet = sheetName ? this.model.getters.getSheetIdByName(sheetName) : this.sheetId;
        const highlight = this.highlights.find((highlight) => {
            if (highlight.range.sheetId !== refSheet)
                return false;
            const range = this.model.getters.getRangeFromSheetXC(refSheet, xc);
            let zone = range.zone;
            zone = getZoneArea(zone) === 1 ? this.model.getters.expandZone(refSheet, zone) : zone;
            return isEqual(zone, highlight.range.zone);
        });
        return highlight && highlight.color ? highlight.color : undefined;
    }
    /**
     * Compute for each token if it is part of the same
     * formula as the current selector token.
     * If no specific formula found for the current selected
     * token, it assumes all tokens are part of the formula
     * context.
     */
    computeFormulaCursorContext() {
        // reset everything first
        for (let i = 0; i < this.currentTokens.length; i++) {
            this.currentTokens[i].isBlurred = false;
        }
        if (this.selectionStart !== this.selectionEnd) {
            return;
        }
        const parenthesesCodeAtCursor = this.getParenthesesCodeAfterCursor();
        const previousSymbolAtCursor = [...this.currentTokens] // a formula correspond to a symbol token
            .reverse()
            .find((t) => parenthesesCodeAtCursor.startsWith(t.parenthesesCode) && t.type === "SYMBOL");
        if (!previousSymbolAtCursor) {
            return;
        }
        // we refer to the previous symbol parenthesesCode and not directly the
        // parenthesesCode of the token at the cursor because we don't want to
        // match cases where the token at the cursor is between parentheses which
        // are not function parentheses
        for (let i = 0; i < this.currentTokens.length; i++) {
            if (!(this.currentTokens[i].parenthesesCode || "").startsWith(previousSymbolAtCursor.parenthesesCode || "")) {
                this.currentTokens[i].isBlurred = true;
            }
        }
    }
    getParenthesesCodeAfterCursor() {
        // we always look at the code associated with the token located after the cursor.
        // This code is the same as the 'tokenAtCursor' except in the case of a closing parenthesis.
        // In this case we look at the code located one degree below in the parentheses tree
        const code = this.tokenAtCursor?.parenthesesCode || "";
        if (this.tokenAtCursor?.type === "RIGHT_PAREN") {
            return code.slice(0, -1) || "";
        }
        return code;
    }
    computeParenthesisRelatedToCursor() {
        // reset everything first
        for (let i = 0; i < this.currentTokens.length; i++) {
            this.currentTokens[i].isParenthesisLinkedToCursor = false;
        }
        const tokenAtCursor = this.tokenAtCursor;
        if (!tokenAtCursor ||
            tokenAtCursor.parenthesesCode === "" ||
            !["LEFT_PAREN", "RIGHT_PAREN"].includes(tokenAtCursor.type)) {
            return;
        }
        for (let i = 0; i < this.currentTokens.length; i++) {
            const currentToken = this.currentTokens[i];
            if (["LEFT_PAREN", "RIGHT_PAREN"].includes(currentToken.type) &&
                currentToken.parenthesesCode === tokenAtCursor.parenthesesCode &&
                currentToken !== tokenAtCursor) {
                this.currentTokens[i].isParenthesisLinkedToCursor = true;
                this.tokenAtCursor.isParenthesisLinkedToCursor = true;
            }
        }
    }
    updateRangeColor() {
        if (!isFormula(this._currentContent) || this.editionMode === "inactive") {
            return;
        }
        const editionSheetId = this.sheetId;
        const XCs = this.getReferencedRanges().map((range) => this.getters.getRangeString(range, editionSheetId));
        const colorsToKeep = {};
        for (const xc of XCs) {
            if (this.colorIndexByRange[xc] !== undefined) {
                colorsToKeep[xc] = this.colorIndexByRange[xc];
            }
        }
        const usedIndexes = new Set(Object.values(colorsToKeep));
        let currentIndex = 0;
        const nextIndex = () => {
            while (usedIndexes.has(currentIndex))
                currentIndex++;
            usedIndexes.add(currentIndex);
            return currentIndex;
        };
        for (const xc of XCs) {
            const colorIndex = xc in colorsToKeep ? colorsToKeep[xc] : nextIndex();
            colorsToKeep[xc] = colorIndex;
        }
        this.colorIndexByRange = colorsToKeep;
    }
    /**
     * Highlight all ranges that can be found in the composer content.
     */
    get highlights() {
        if (!isFormula(this.currentContent) || this.editionMode === "inactive") {
            return [];
        }
        const editionSheetId = this.sheetId;
        const rangeColor = (rangeString) => {
            const colorIndex = this.colorIndexByRange[rangeString];
            return colors[colorIndex % colors.length];
        };
        return this.getReferencedRanges().map((range) => {
            const rangeString = this.getters.getRangeString(range, editionSheetId);
            const { numberOfRows, numberOfCols } = zoneToDimension(range.zone);
            const zone = numberOfRows * numberOfCols === 1
                ? this.getters.expandZone(range.sheetId, range.zone)
                : range.unboundedZone;
            return {
                range: this.model.getters.getRangeFromZone(range.sheetId, zone),
                color: rangeColor(rangeString),
                interactive: true,
            };
        });
    }
    /**
     * Return ranges currently referenced in the composer
     */
    getReferencedRanges() {
        const editionSheetId = this.sheetId;
        const referenceRanges = this.currentTokens
            .filter((token) => token.type === "REFERENCE")
            .map((token) => this.getters.getRangeFromSheetXC(editionSheetId, token.value));
        return referenceRanges.filter((range) => !range.invalidSheetName && !range.invalidXc);
    }
    async updateAutoCompleteProvider() {
        this.autoComplete.hide();
        const provider = await this.autoCompleteKeepLast.add(this.findAutocompleteProvider());
        if (provider) {
            this.autoComplete.useProvider(provider);
            this.model.trigger("update");
        }
    }
    async findAutocompleteProvider() {
        const content = this.currentContent;
        const tokenAtCursor = isFormula(content)
            ? this.tokenAtCursor
            : { type: "STRING", value: content };
        if (this.editionMode === "inactive" ||
            !tokenAtCursor ||
            ["TRUE", "FALSE"].includes(tokenAtCursor.value.toUpperCase()) ||
            !(this.canStartComposerRangeSelection() ||
                ["SYMBOL", "STRING", "UNKNOWN"].includes(tokenAtCursor.type))) {
            return;
        }
        const thisCtx = { composer: this, getters: this.getters };
        const providersDefinitions = this.getAutoCompleteProviders();
        const providers = providersDefinitions
            .sort((a, b) => (a.sequence ?? Infinity) - (b.sequence ?? Infinity))
            .map((provider) => ({
            ...provider,
            getProposals: provider.getProposals.bind(thisCtx, tokenAtCursor, content),
            selectProposal: provider.selectProposal.bind(thisCtx, tokenAtCursor),
        }));
        for (const provider of providers) {
            let proposals = await provider.getProposals();
            const exactMatch = proposals?.find((p) => p.text === tokenAtCursor.value);
            // remove tokens that are likely to be other parts of the formula that slipped in the token if it's a string
            const searchTerm = tokenAtCursor.value.replace(/[ ,\(\)]/g, "");
            if (this._currentContent === this.initialContent &&
                provider.displayAllOnInitialContent &&
                proposals?.length) {
                return {
                    proposals,
                    selectProposal: provider.selectProposal,
                    autoSelectFirstProposal: provider.autoSelectFirstProposal ?? false,
                    canBeToggled: provider.canBeToggled,
                };
            }
            if (exactMatch && this._currentContent !== this.initialContent) {
                // this means the user has chosen a proposal
                return;
            }
            if (searchTerm &&
                proposals &&
                !["ARG_SEPARATOR", "LEFT_PAREN", "OPERATOR"].includes(tokenAtCursor.type)) {
                const filteredProposals = fuzzyLookup(searchTerm, proposals, (p) => p.fuzzySearchKey || p.text);
                if (!exactMatch || filteredProposals.length) {
                    proposals = filteredProposals;
                }
            }
            if (provider.maxDisplayedProposals) {
                proposals = proposals?.slice(0, provider.maxDisplayedProposals);
            }
            if (proposals?.length) {
                return {
                    proposals,
                    selectProposal: provider.selectProposal,
                    autoSelectFirstProposal: provider.autoSelectFirstProposal ?? false,
                    canBeToggled: provider.canBeToggled,
                };
            }
        }
        return;
    }
    hideHelp() {
        this.autoComplete.hide();
    }
    autoCompleteOrStop(direction, assistantForcedClosed = false) {
        if (this.editionMode !== "inactive") {
            const autoComplete = this.autoComplete;
            const suppressAutocomplete = assistantForcedClosed && this.canBeToggled;
            if (!suppressAutocomplete &&
                autoComplete.provider &&
                autoComplete.selectedIndex !== undefined) {
                const autoCompleteValue = autoComplete.provider.proposals[autoComplete.selectedIndex]?.text;
                if (autoCompleteValue) {
                    this.autoComplete.provider?.selectProposal(autoCompleteValue);
                    return;
                }
            }
            this.stopEdition(direction);
        }
    }
    insertAutoCompleteValue(value) {
        this.autoComplete.provider?.selectProposal(value);
    }
    selectAutoCompleteIndex(index) {
        this.autoComplete.selectIndex(clip(0, index, 10));
    }
    moveAutoCompleteSelection(direction) {
        this.autoComplete.moveSelection(direction);
    }
    /**
     * Function used to determine when composer selection can start.
     * Three conditions are necessary:
     * - the previous token is among ["ARG_SEPARATOR", "LEFT_PAREN", "OPERATOR"], and is not a postfix unary operator
     * - the next token is missing or is among ["ARG_SEPARATOR", "RIGHT_PAREN", "OPERATOR"]
     * - Previous and next tokens can be separated by spaces
     */
    canStartComposerRangeSelection() {
        if (isFormula(this._currentContent)) {
            const tokenAtCursor = this.tokenAtCursor;
            if (!tokenAtCursor) {
                return false;
            }
            const tokenIdex = this.currentTokens.map((token) => token.start).indexOf(tokenAtCursor.start);
            let count = tokenIdex;
            let currentToken = tokenAtCursor;
            // check previous token
            while (!["ARG_SEPARATOR", "LEFT_PAREN", "OPERATOR"].includes(currentToken.type) ||
                POSTFIX_UNARY_OPERATORS.includes(currentToken.value)) {
                if (currentToken.type !== "SPACE" || count < 1) {
                    return false;
                }
                count--;
                currentToken = this.currentTokens[count];
            }
            count = tokenIdex + 1;
            currentToken = this.currentTokens[count];
            // check next token
            while (currentToken &&
                !["ARG_SEPARATOR", "RIGHT_PAREN", "OPERATOR"].includes(currentToken.type)) {
                if (currentToken.type !== "SPACE") {
                    return false;
                }
                count++;
                currentToken = this.currentTokens[count];
            }
            return true;
        }
        return false;
    }
    getNumberOfMissingParenthesis(tokens) {
        const left = tokens.filter((t) => t.type === "LEFT_PAREN").length;
        const right = tokens.filter((t) => t.type === "RIGHT_PAREN").length;
        return left - right;
    }
}

class StandaloneComposerStore extends AbstractComposerStore {
    args;
    constructor(get, args) {
        super(get);
        this.args = args;
        this._currentContent = this.getComposerContent().text;
    }
    getAutoCompleteProviders() {
        const providersDefinitions = super.getAutoCompleteProviders();
        const contextualAutocomplete = this.args().contextualAutocomplete;
        if (contextualAutocomplete) {
            providersDefinitions.push(contextualAutocomplete);
        }
        return providersDefinitions;
    }
    /**
     * Replace the current reference selected by the new one.
     * */
    getZoneReference(zone) {
        const res = super.getZoneReference(zone);
        if (this.args().defaultStatic) {
            return setXcToFixedReferenceType(res, "colrow");
        }
        return res;
    }
    getComposerContent() {
        let content = this._currentContent;
        if (this.editionMode === "inactive") {
            // References in the content might not be linked to the current active sheet
            // We here force the sheet name prefix for all references that are not in
            // the current active sheet
            const defaultRangeSheetId = this.args().defaultRangeSheetId;
            content = rangeTokenize(this.args().content)
                .map((token) => {
                if (token.type === "REFERENCE") {
                    const range = this.getters.getRangeFromSheetXC(defaultRangeSheetId, token.value);
                    return this.getters.getRangeString(range, this.getters.getActiveSheetId());
                }
                return token.value;
            })
                .join("");
        }
        return { text: localizeContent(content, this.getters.getLocale()) };
    }
    stopEdition() {
        this._stopEdition();
    }
    confirmEdition(content) {
        this.args().onConfirm(content);
    }
    getTokenColor(token) {
        if (token.type === "SYMBOL") {
            const matchedColor = this.args().getContextualColoredSymbolToken?.(token);
            if (matchedColor) {
                return matchedColor;
            }
        }
        return super.getTokenColor(token);
    }
    hoverToken() {
        /**
         * Some functions can only be evaluated in the context of the grid, which is not feasible
         * in the standalone composer.
         */
        return;
    }
}

css /* scss */ `
  .o-spreadsheet {
    .o-standalone-composer {
      min-height: 24px;

      border-bottom: 1px solid;
      border-color: ${GRAY_300};

      &.active {
        border-color: ${ACTION_COLOR};
      }

      &.o-invalid {
        border-bottom: 2px solid red;
      }

      /* As the standalone composer is potentially very small (eg. in a side panel), we remove the scrollbar display */
      scrollbar-width: none; /* Firefox */
      &::-webkit-scrollbar {
        display: none;
      }
    }
  }
`;
class StandaloneComposer extends Component {
    static template = "o-spreadsheet-StandaloneComposer";
    static props = {
        composerContent: { type: String, optional: true },
        defaultRangeSheetId: { type: String, optional: true },
        defaultStatic: { type: Boolean, optional: true },
        onConfirm: Function,
        contextualAutocomplete: { type: Object, optional: true },
        placeholder: { type: String, optional: true },
        title: { type: String, optional: true },
        class: { type: String, optional: true },
        invalid: { type: Boolean, optional: true },
        getContextualColoredSymbolToken: { type: Function, optional: true },
    };
    static components = { Composer };
    static defaultProps = {
        composerContent: "",
        defaultStatic: false,
    };
    composerFocusStore;
    standaloneComposerStore;
    composerInterface;
    spreadsheetRect = useSpreadsheetRect();
    setup() {
        this.composerFocusStore = useStore(ComposerFocusStore);
        const standaloneComposerStore = useLocalStore(StandaloneComposerStore, () => ({
            onConfirm: this.props.onConfirm,
            content: this.props.composerContent,
            defaultStatic: this.props.defaultStatic ?? false,
            contextualAutocomplete: this.props.contextualAutocomplete,
            defaultRangeSheetId: this.props.defaultRangeSheetId,
            getContextualColoredSymbolToken: this.props.getContextualColoredSymbolToken,
        }));
        this.standaloneComposerStore = standaloneComposerStore;
        this.composerInterface = {
            id: "standaloneComposer",
            get editionMode() {
                return standaloneComposerStore.editionMode;
            },
            startEdition: this.standaloneComposerStore.startEdition,
            setCurrentContent: this.standaloneComposerStore.setCurrentContent,
            stopEdition: this.standaloneComposerStore.stopEdition,
        };
    }
    get focus() {
        return this.composerFocusStore.activeComposer === this.composerInterface
            ? this.composerFocusStore.focusMode
            : "inactive";
    }
    get composerStyle() {
        return this.props.invalid
            ? cssPropertiesToCss({ padding: "1px 0px 0px 0px" })
            : cssPropertiesToCss({ padding: "1px 0px" });
    }
    get containerClass() {
        const classes = [
            this.focus === "inactive" ? "" : "active",
            this.props.invalid ? "o-invalid" : "",
            this.props.class || "",
        ];
        return classes.join(" ");
    }
    onFocus(selection) {
        this.composerFocusStore.focusComposer(this.composerInterface, { selection });
    }
}

css /* scss */ `
  .o-dv-input {
    &.o-invalid {
      background-color: #ffdddd;
    }
    .error-icon {
      right: 7px;
      top: 7px;
    }
  }
`;
class CriterionInput extends Component {
    static template = "o-spreadsheet-CriterionInput";
    static props = {
        value: { type: String, optional: true },
        criterionType: String,
        onValueChanged: Function,
        onKeyDown: { type: Function, optional: true },
        focused: { type: Boolean, optional: true },
        onBlur: { type: Function, optional: true },
        onFocus: { type: Function, optional: true },
        disableFormulas: { type: Boolean, optional: true },
    };
    static defaultProps = {
        value: "",
        onKeyDown: () => { },
        focused: false,
        onBlur: () => { },
    };
    static components = { StandaloneComposer: StandaloneComposer };
    inputRef = useRef("input");
    setup() {
        useEffect(() => {
            if (this.props.focused) {
                this.inputRef.el.focus();
            }
        }, () => [this.props.focused, this.inputRef.el]);
    }
    state = useState({
        shouldDisplayError: !!this.props.value, // Don't display error if user inputted nothing yet
    });
    get placeholder() {
        if (this.allowedValues === "onlyFormulas") {
            return _t("Formula");
        }
        else if (this.allowedValues === "onlyLiterals") {
            return _t("Value");
        }
        return _t("Value or formula");
    }
    get allowedValues() {
        const evaluator = criterionEvaluatorRegistry.get(this.props.criterionType);
        if (evaluator.allowedValues === "onlyFormulas" && this.props.disableFormulas) {
            throw new Error(`Cannot disable formulas for criterion type ${this.props.criterionType} that accept only formulas`);
        }
        const allowedValues = this.props.disableFormulas ? "onlyLiterals" : evaluator.allowedValues;
        return allowedValues ?? "any";
    }
    onInputValueChanged(ev) {
        this.state.shouldDisplayError = true;
        this.props.onValueChanged(ev.target.value);
    }
    onChangeComposerValue(str) {
        this.state.shouldDisplayError = true;
        this.props.onValueChanged(str);
    }
    getDataValidationRuleInputComposerProps() {
        return {
            onConfirm: (str) => this.onChangeComposerValue(str),
            composerContent: this.props.value,
            placeholder: this.placeholder,
            class: "o-sidePanel-composer",
            defaultRangeSheetId: this.env.model.getters.getActiveSheetId(),
            invalid: this.state.shouldDisplayError && !!this.errorMessage,
            defaultStatic: true,
        };
    }
    get errorMessage() {
        if (!this.state.shouldDisplayError) {
            return undefined;
        }
        return this.env.model.getters.getDataValidationInvalidCriterionValueMessage(this.props.criterionType, canonicalizeContent(this.props.value, this.env.model.getters.getLocale()));
    }
}

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

class DoubleInputCriterionForm extends CriterionForm {
    static template = "o-spreadsheet-DoubleInputCriterionForm";
    static components = { CriterionInput };
    onFirstValueChanged(value) {
        const values = this.props.criterion.values;
        this.updateCriterion({
            values: [value, values[1] || ""],
        });
    }
    onSecondValueChanged(value) {
        const values = this.props.criterion.values;
        this.updateCriterion({
            values: [values[0] || "", value],
        });
    }
}

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

/**
 * Start listening to pointer events and apply the given callbacks.
 *
 * @returns A function to remove the listeners.
 */
function startDnd(onPointerMove, onPointerUp) {
    const removeListeners = () => {
        window.removeEventListener("pointerup", _onPointerUp, { capture: true });
        window.removeEventListener("dragstart", _onDragStart);
        window.removeEventListener("pointermove", onPointerMove);
        window.removeEventListener("wheel", onPointerMove);
    };
    const _onPointerUp = (ev) => {
        onPointerUp(ev);
        removeListeners();
    };
    function _onDragStart(ev) {
        ev.preventDefault();
    }
    window.addEventListener("pointerup", _onPointerUp, { capture: true });
    window.addEventListener("dragstart", _onDragStart);
    window.addEventListener("pointermove", onPointerMove);
    // mouse wheel on window is by default a passive event.
    // preventDefault() is not allowed in passive event handler.
    // https://chromestatus.com/feature/6662647093133312
    window.addEventListener("wheel", onPointerMove, { passive: false });
    return removeListeners;
}

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

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

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

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

class Section extends Component {
    static template = "o_spreadsheet.Section";
    static props = {
        class: { type: String, optional: true },
        title: { type: String, optional: true },
        slots: Object,
    };
}

const TRANSPARENT_BACKGROUND_SVG = /*xml*/ `
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10">
  <path fill="#d9d9d9" d="M5 5h5v5H5zH0V0h5"/>
</svg>
`;
css /* scss */ `
  .o-round-color-picker-button {
    width: 20px;
    height: 20px;
    cursor: pointer;
    border: 1px solid ${GRAY_300};
    background-position: 1px 1px;
    background-image: url("data:image/svg+xml,${encodeURIComponent(TRANSPARENT_BACKGROUND_SVG)}");
  }
`;
class RoundColorPicker extends Component {
    static template = "o-spreadsheet.RoundColorPicker";
    static components = { Section, ColorPicker };
    static props = {
        currentColor: { type: String, optional: true },
        title: { type: String, optional: true },
        onColorPicked: Function,
        disableNoColor: { type: Boolean, optional: true },
    };
    colorPickerButtonRef = useRef("colorPickerButton");
    state;
    setup() {
        this.state = useState({ pickerOpened: false });
        useExternalListener(window, "click", this.closePicker);
    }
    closePicker() {
        this.state.pickerOpened = false;
    }
    togglePicker() {
        this.state.pickerOpened = !this.state.pickerOpened;
    }
    onColorPicked(color) {
        this.props.onColorPicked(color);
        this.state.pickerOpened = false;
    }
    get colorPickerAnchorRect() {
        const button = this.colorPickerButtonRef.el;
        return getBoundingRectAsPOJO(button);
    }
    get buttonStyle() {
        return cssPropertiesToCss({
            background: this.props.currentColor,
        });
    }
}

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

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

// The name is misleading and can be confused with the DOM focus.
class FocusStore {
    mutators = ["focus", "unfocus"];
    focusedElement = null;
    focus(element) {
        this.focusedElement = element;
    }
    unfocus(element) {
        if (this.focusedElement && this.focusedElement === element) {
            this.focusedElement = null;
        }
    }
}

/**
 * Selection input Plugin
 *
 * The SelectionInput component input and output are both arrays of strings, but
 * it requires an intermediary internal state to work.
 * This plugin handles this internal state.
 */
class SelectionInputStore extends SpreadsheetStore {
    initialRanges;
    inputHasSingleRange;
    colors;
    disabledRanges;
    mutators = [
        "resetWithRanges",
        "focusById",
        "unfocus",
        "addEmptyRange",
        "removeRange",
        "changeRange",
        "reset",
        "confirm",
        "updateColors",
        "updateDisabledRanges",
    ];
    ranges = [];
    focusedRangeIndex = null;
    inputSheetId;
    focusStore = this.get(FocusStore);
    highlightStore = this.get(HighlightStore);
    constructor(get, initialRanges = [], inputHasSingleRange = false, colors = [], disabledRanges = []) {
        super(get);
        this.initialRanges = initialRanges;
        this.inputHasSingleRange = inputHasSingleRange;
        this.colors = colors;
        this.disabledRanges = disabledRanges;
        if (inputHasSingleRange && initialRanges.length > 1) {
            throw new Error("Input with a single range cannot be instantiated with several range references.");
        }
        this.inputSheetId = this.getters.getActiveSheetId();
        this.resetWithRanges(initialRanges);
        this.highlightStore.register(this);
        this.onDispose(() => {
            this.unfocus();
            this.highlightStore.unRegister(this);
        });
    }
    handleEvent(event) {
        if (this.focusedRangeIndex === null) {
            return;
        }
        const inputSheetId = this.inputSheetId;
        const activeSheetId = this.getters.getActiveSheetId();
        const zone = event.options.unbounded
            ? this.getters.getUnboundedZone(activeSheetId, event.anchor.zone)
            : event.anchor.zone;
        const range = this.getters.getRangeFromZone(activeSheetId, zone);
        const willAddNewRange = event.mode === "newAnchor" &&
            !this.inputHasSingleRange &&
            this.ranges[this.focusedRangeIndex].xc.trim() !== "";
        if (willAddNewRange) {
            const xc = this.getters.getSelectionRangeString(range, inputSheetId);
            this.insertNewRange(this.ranges.length, [xc]);
            this.focusLast();
        }
        else {
            let parts = range.parts;
            const previousXc = this.ranges[this.focusedRangeIndex].xc.trim();
            if (previousXc) {
                parts = this.getters.getRangeFromSheetXC(inputSheetId, previousXc).parts;
            }
            const newRange = { ...range, parts };
            const xc = this.getters.getSelectionRangeString(newRange, inputSheetId);
            this.setRange(this.focusedRangeIndex, [xc]);
        }
    }
    handle(cmd) {
        switch (cmd.type) {
            case "ACTIVATE_SHEET": {
                if (cmd.sheetIdFrom !== cmd.sheetIdTo) {
                    const { col, row } = this.getters.getNextVisibleCellPosition({
                        sheetId: cmd.sheetIdTo,
                        col: 0,
                        row: 0,
                    });
                    const zone = this.getters.expandZone(cmd.sheetIdTo, positionToZone({ col, row }));
                    this.model.selection.resetAnchor(this, { cell: { col, row }, zone });
                }
                break;
            }
            case "START_CHANGE_HIGHLIGHT":
                const activeSheetId = this.getters.getActiveSheetId();
                const newZone = this.getters.expandZone(activeSheetId, cmd.zone);
                const focusIndex = this.ranges.findIndex((range) => {
                    const { xc, sheetName: sheet } = splitReference(range.xc);
                    const sheetName = sheet || this.getters.getSheetName(this.inputSheetId);
                    if (this.getters.getSheetName(activeSheetId) !== sheetName) {
                        return false;
                    }
                    const refRange = this.getters.getRangeFromSheetXC(activeSheetId, xc);
                    return isEqual(this.getters.expandZone(activeSheetId, refRange.zone), newZone);
                });
                if (focusIndex !== -1) {
                    this.focus(focusIndex);
                    const { left, top } = newZone;
                    this.model.selection.resetAnchor(this, {
                        cell: { col: left, row: top },
                        zone: newZone,
                    });
                }
                break;
        }
    }
    changeRange(rangeId, value) {
        if (this.inputHasSingleRange && value.split(",").length > 1) {
            return;
        }
        const index = this.getIndex(rangeId);
        if (index !== null && this.focusedRangeIndex !== index) {
            this.focus(index);
        }
        if (index !== null) {
            const valueWithoutLeadingComma = value.replace(/^,+/, "");
            const values = valueWithoutLeadingComma.split(",").map((reference) => reference.trim());
            this.setRange(index, values);
            this.captureSelection();
        }
    }
    addEmptyRange() {
        if (this.inputHasSingleRange && this.ranges.length === 1) {
            return;
        }
        this.insertNewRange(this.ranges.length, [""]);
        this.focusLast();
    }
    removeRange(rangeId) {
        if (this.ranges.length === 1) {
            return;
        }
        const index = this.getIndex(rangeId);
        if (index !== null) {
            this.removeRangeByIndex(index);
        }
    }
    updateColors(colors) {
        this.colors = colors;
        const colorGenerator = new ColorGenerator(this.ranges.length, this.colors);
        this.ranges = this.ranges.map((range) => ({
            ...range,
            color: colorGenerator.next(),
        }));
    }
    updateDisabledRanges(disabledRanges) {
        this.disabledRanges = disabledRanges;
    }
    confirm() {
        for (const range of this.selectionInputs) {
            if (range.xc === "") {
                this.removeRange(range.id);
            }
        }
        const activeSheetId = this.getters.getActiveSheetId();
        if (this.inputSheetId !== activeSheetId) {
            this.model.dispatch("ACTIVATE_SHEET", {
                sheetIdFrom: activeSheetId,
                sheetIdTo: this.inputSheetId,
            });
        }
        if (this.selectionInputValues.join() !== this.initialRanges.join()) {
            this.resetWithRanges(this.selectionInputValues);
        }
        this.initialRanges = this.selectionInputValues;
        this.unfocus();
    }
    reset() {
        this.resetWithRanges(this.initialRanges);
        this.confirm();
    }
    get selectionInputValues() {
        return this.cleanInputs(this.ranges.map((range) => {
            return range.xc ? range.xc : "";
        }));
    }
    /**
     * Return a list of all valid XCs.
     * e.g. ["A1", "Sheet2!B3", "E12"]
     */
    get selectionInputs() {
        return this.ranges.map((input, index) => Object.assign({}, input, {
            color: this.hasMainFocus &&
                this.focusedRangeIndex !== null &&
                this.getters.isRangeValid(input.xc)
                ? input.color
                : null,
            isFocused: this.hasMainFocus && this.focusedRangeIndex === index,
            isValidRange: input.xc === "" || this.getters.isRangeValid(input.xc),
            disabled: this.disabledRanges?.[index],
        }));
    }
    get isResettable() {
        return this.initialRanges.join() !== this.ranges.map((r) => r.xc).join();
    }
    get isConfirmable() {
        return this.selectionInputs.every((range) => range.isValidRange);
    }
    get hasFocus() {
        return this.selectionInputs.some((i) => i.isFocused);
    }
    get hasMainFocus() {
        const focusedElement = this.focusStore.focusedElement;
        return !!focusedElement && focusedElement === this;
    }
    get highlights() {
        if (!this.hasMainFocus) {
            return [];
        }
        // TODO expand zone globally
        return this.ranges.map((input) => this.inputToHighlights(input)).flat();
    }
    // ---------------------------------------------------------------------------
    // Other
    // ---------------------------------------------------------------------------
    focusById(rangeId) {
        this.focus(this.getIndex(rangeId));
    }
    /**
     * Focus a given range or remove the focus.
     */
    focus(index) {
        this.focusStore.focus(this);
        this.focusedRangeIndex = index;
        this.captureSelection();
    }
    focusLast() {
        this.focus(this.ranges.length - 1);
    }
    unfocus() {
        this.focusedRangeIndex = null;
        this.focusStore.unfocus(this);
        this.model.selection.release(this);
    }
    captureSelection() {
        if (this.focusedRangeIndex === null) {
            return;
        }
        const range = this.ranges[this.focusedRangeIndex];
        const sheetId = this.getters.getActiveSheetId();
        const zone = this.getters.getRangeFromSheetXC(sheetId, range?.xc || "A1").zone;
        this.model.selection.capture(this, { cell: { col: zone.left, row: zone.top }, zone }, {
            handleEvent: this.handleEvent.bind(this),
            release: this.unfocus.bind(this),
        });
    }
    resetWithRanges(ranges) {
        if (ranges.length && ranges.every((r) => this.getters.isRangeValid(r))) {
            this.initialRanges = ranges;
        }
        this.ranges = [];
        this.insertNewRange(0, ranges);
        if (this.ranges.length === 0) {
            this.insertNewRange(this.ranges.length, [""]);
            this.focusLast();
        }
    }
    setContent(index, xc) {
        this.ranges[index] = {
            ...this.ranges[index],
            xc,
        };
    }
    /**
     * Insert new inputs after the given index.
     */
    insertNewRange(index, values) {
        const currentMaxId = Math.max(0, ...this.ranges.map((range) => Number(range.id)));
        const colors = new ColorGenerator(this.ranges.length, this.colors);
        for (let i = 0; i < index; i++) {
            colors.next();
        }
        this.ranges.splice(index, 0, ...values.map((xc, i) => ({
            xc,
            id: currentMaxId + i + 1,
            color: colors.next(),
        })));
    }
    /**
     * 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);
        }
    }
    removeRangeByIndex(index) {
        this.ranges.splice(index, 1);
        if (this.focusedRangeIndex !== null) {
            this.focusLast();
        }
    }
    /**
     * Converts highlights input format to the command format.
     * The first xc in the input range will keep its color.
     * Invalid ranges and ranges from other sheets than the active sheets
     * are ignored.
     */
    inputToHighlights({ xc, color }) {
        const XCs = this.cleanInputs([xc])
            .filter((range) => this.getters.isRangeValid(range))
            .filter((reference) => this.shouldBeHighlighted(this.inputSheetId, reference));
        return XCs.map((xc) => {
            const { sheetName } = splitReference(xc);
            const sheetId = (sheetName && this.getters.getSheetIdByName(sheetName)) || this.inputSheetId;
            return {
                range: this.getters.getRangeFromSheetXC(sheetId, xc),
                color,
                interactive: true,
            };
        });
    }
    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;
    }
}

css /* scss */ `
  .o-selection {
    .o-selection-input {
      padding: 2px 0px;

      input.o-invalid {
        background-color: ${ALERT_DANGER_BG};
      }
      .input-icon {
        right: 7px;
        top: 4px;
      }
      .o-drag-handle {
        cursor: move;
      }
    }
    .o-disabled-ranges {
      color: #888 !important;
    }
    .o-button {
      flex-grow: 0;
    }

    /* Make the character a bit bigger
    compared to its neighbor INPUT box  */
    .o-remove-selection {
      font-size: calc(100% + 4px);
    }
  }
`;
/**
 * This component can be used when the user needs to input some
 * ranges. He can either input the ranges with the regular DOM `<input/>`
 * displayed or by selecting zones on the grid.
 *
 * onSelectionChanged is called every time the input value
 * changes.
 */
class SelectionInput extends Component {
    static template = "o-spreadsheet-SelectionInput";
    static props = {
        ranges: Array,
        hasSingleRange: { type: Boolean, optional: true },
        required: { type: Boolean, optional: true },
        isInvalid: { type: Boolean, optional: true },
        class: { type: String, optional: true },
        onSelectionChanged: { type: Function, optional: true },
        onSelectionConfirmed: { type: Function, optional: true },
        onSelectionReordered: { type: Function, optional: true },
        onSelectionRemoved: { type: Function, optional: true },
        colors: { type: Array, optional: true, default: [] },
        disabledRanges: { type: Array, optional: true, default: [] },
        disabledRangeTitle: { type: String, optional: true },
    };
    state = useState({
        isMissing: false,
        mode: "select-range",
    });
    dragAndDrop = useDragAndDropListItems();
    focusedInput = useRef("focusedInput");
    selectionRef = useRef("o-selection");
    store;
    get ranges() {
        return this.store.selectionInputs;
    }
    get canAddRange() {
        return !this.props.hasSingleRange;
    }
    get isInvalid() {
        return this.props.isInvalid || this.state.isMissing;
    }
    get isConfirmable() {
        return this.store.isConfirmable;
    }
    get isResettable() {
        return this.store.isResettable;
    }
    get hasDisabledRanges() {
        return this.store.disabledRanges.some(Boolean);
    }
    setup() {
        useEffect(() => this.focusedInput.el?.focus(), () => [this.focusedInput.el]);
        useEffect(() => {
            // Check the offsetParent to know if the input or an ancestor is `display: none` (eg. when changing side panel tab)
            if ((this.store.isResettable || this.store.hasFocus) &&
                this.selectionRef.el?.offsetParent === null) {
                this.reset();
            }
        });
        this.store = useLocalStore(SelectionInputStore, this.props.ranges, this.props.hasSingleRange || false, this.props.colors, this.props.disabledRanges);
        onWillUpdateProps((nextProps) => {
            if (nextProps.ranges.join() !== this.store.selectionInputValues.join()) {
                this.triggerChange();
            }
            if (nextProps.ranges.join() !== this.props.ranges.join() &&
                nextProps.ranges.join() !== this.store.selectionInputValues.join()) {
                this.store.resetWithRanges(nextProps.ranges);
            }
            if (nextProps.colors?.join() !== this.props.colors?.join() &&
                nextProps.colors?.join() !== this.store.colors.join()) {
                this.store.updateColors(nextProps.colors || []);
            }
            if (!deepEquals(nextProps.disabledRanges, this.props.disabledRanges) &&
                !deepEquals(nextProps.disabledRanges, this.store.disabledRanges)) {
                this.store.updateDisabledRanges(nextProps.disabledRanges || []);
            }
        });
    }
    startDragAndDrop(rangeId, event) {
        if (event.button !== 0 || event.target.tagName === "SELECT") {
            return;
        }
        const rects = this.getRangeElementsRects();
        const draggableIds = this.ranges.map((range) => range.id);
        const draggableItems = draggableIds.map((id, index) => ({
            id: id.toString(),
            size: rects[index].height,
            position: rects[index].y,
        }));
        this.dragAndDrop.start("vertical", {
            draggedItemId: rangeId.toString(),
            initialMousePosition: event.clientY,
            items: draggableItems,
            scrollableContainerEl: this.selectionRef.el,
            onDragEnd: (dimensionName, finalIndex) => {
                const originalIndex = draggableIds.findIndex((id) => id === rangeId);
                if (originalIndex === finalIndex) {
                    return;
                }
                const indexes = range(0, draggableIds.length);
                indexes.splice(originalIndex, 1);
                indexes.splice(finalIndex, 0, originalIndex);
                this.props.onSelectionReordered?.(indexes);
                this.props.onSelectionConfirmed?.();
                this.store.confirm();
            },
        });
    }
    getRangeElementsRects() {
        return Array.from(this.selectionRef.el.children).map((el) => el.getBoundingClientRect());
    }
    getColor(range) {
        if (!range.color) {
            return "";
        }
        return cssPropertiesToCss({ color: range.color });
    }
    triggerChange() {
        const ranges = this.store.selectionInputValues;
        this.props.onSelectionChanged?.(ranges);
    }
    onKeydown(ev) {
        if (ev.key === "F2") {
            ev.preventDefault();
            ev.stopPropagation();
            this.state.mode = this.state.mode === "select-range" ? "text-edit" : "select-range";
        }
        else if (ev.key.startsWith("Arrow")) {
            ev.stopPropagation();
            if (this.state.mode === "select-range") {
                ev.preventDefault();
                updateSelectionWithArrowKeys(ev, this.env.model.selection);
            }
        }
        else if (ev.key === "Enter") {
            const target = ev.target;
            target.blur();
            if (this.isConfirmable) {
                this.confirm();
            }
        }
    }
    extractRanges(value) {
        return this.props.hasSingleRange ? value.split(",")[0] : value;
    }
    focus(rangeId) {
        this.state.isMissing = false;
        this.state.mode = "select-range";
        this.store.focusById(rangeId);
    }
    addEmptyInput() {
        this.store.addEmptyRange();
    }
    removeInput(rangeId) {
        const index = this.store.selectionInputs.findIndex((range) => range.id === rangeId);
        if (this.ranges.find((range) => range.id === rangeId)?.xc) {
            this.props.onSelectionRemoved?.(index);
            this.props.onSelectionConfirmed?.();
        }
        this.store.removeRange(rangeId);
    }
    onInputChanged(rangeId, ev) {
        const target = ev.target;
        const value = this.extractRanges(target.value);
        this.store.changeRange(rangeId, value);
        this.triggerChange();
    }
    reset() {
        this.store.reset();
        this.triggerChange();
    }
    confirm() {
        this.store.confirm();
        const anyValidInput = this.store.selectionInputs.some((range) => this.env.model.getters.isRangeValid(range.xc));
        if (this.props.required && !anyValidInput) {
            this.state.isMissing = true;
        }
        this.props.onSelectionChanged?.(this.store.selectionInputValues);
        this.props.onSelectionConfirmed?.();
    }
}

class ValueInRangeCriterionForm extends CriterionForm {
    static template = "o-spreadsheet-ValueInRangeCriterionForm";
    static components = { RoundColorPicker, SelectionInput };
    setup() {
        super.setup();
        const setupDefault = (props) => {
            if (props.criterion.displayStyle === undefined) {
                this.updateCriterion({ displayStyle: "chip" });
            }
        };
        onWillUpdateProps(setupDefault);
        onWillStart(() => setupDefault(this.props));
    }
    onRangeChanged(rangeXc) {
        this.updateCriterion({ values: [rangeXc] });
    }
    onChangedDisplayStyle(ev) {
        const displayStyle = ev.target.value;
        this.updateCriterion({ displayStyle });
    }
    onColorChanged(color, value) {
        const colors = { ...this.props.criterion.colors };
        colors[value] = color || undefined;
        this.updateCriterion({ colors });
    }
    get values() {
        const sheetId = this.env.model.getters.getActiveSheetId();
        const values = this.env.model.getters.getDataValidationRangeValues(sheetId, this.props.criterion);
        return new Set(values);
    }
}

const criterionCategoriesSequences = {
    list: 10,
    text: 20,
    date: 30,
    number: 40,
    misc: 50,
};
const criterionComponentRegistry = new Registry();
criterionComponentRegistry.add("containsText", {
    type: "containsText",
    component: SingleInputCriterionForm,
    category: "text",
    sequence: 10,
});
criterionComponentRegistry.add("notContainsText", {
    type: "notContainsText",
    component: SingleInputCriterionForm,
    category: "text",
    sequence: 20,
});
criterionComponentRegistry.add("beginsWithText", {
    type: "beginsWithText",
    component: SingleInputCriterionForm,
    category: "text",
    sequence: 25,
});
criterionComponentRegistry.add("endsWithText", {
    type: "endsWithText",
    component: SingleInputCriterionForm,
    category: "text",
    sequence: 26,
});
criterionComponentRegistry.add("isEqualText", {
    type: "isEqualText",
    component: SingleInputCriterionForm,
    category: "text",
    sequence: 30,
});
criterionComponentRegistry.add("isEmail", {
    type: "isEmail",
    component: undefined,
    category: "text",
    sequence: 40,
});
criterionComponentRegistry.add("isLink", {
    type: "isLink",
    component: undefined,
    category: "text",
    sequence: 50,
});
criterionComponentRegistry.add("dateIs", {
    type: "dateIs",
    component: DateCriterionForm,
    category: "date",
    sequence: 20,
});
criterionComponentRegistry.add("dateIsBefore", {
    type: "dateIsBefore",
    component: DateCriterionForm,
    category: "date",
    sequence: 30,
});
criterionComponentRegistry.add("dateIsOnOrBefore", {
    type: "dateIsOnOrBefore",
    component: DateCriterionForm,
    category: "date",
    sequence: 40,
});
criterionComponentRegistry.add("dateIsAfter", {
    type: "dateIsAfter",
    component: DateCriterionForm,
    category: "date",
    sequence: 50,
});
criterionComponentRegistry.add("dateIsOnOrAfter", {
    type: "dateIsOnOrAfter",
    component: DateCriterionForm,
    category: "date",
    sequence: 60,
});
criterionComponentRegistry.add("dateIsBetween", {
    type: "dateIsBetween",
    component: DoubleInputCriterionForm,
    category: "date",
    sequence: 70,
});
criterionComponentRegistry.add("dateIsNotBetween", {
    type: "dateIsNotBetween",
    component: DoubleInputCriterionForm,
    category: "date",
    sequence: 80,
});
criterionComponentRegistry.add("dateIsValid", {
    type: "dateIsValid",
    component: undefined,
    category: "date",
    sequence: 10,
});
criterionComponentRegistry.add("isEqual", {
    type: "isEqual",
    component: SingleInputCriterionForm,
    category: "number",
    sequence: 10,
});
criterionComponentRegistry.add("isNotEqual", {
    type: "isNotEqual",
    component: SingleInputCriterionForm,
    category: "number",
    sequence: 20,
});
criterionComponentRegistry.add("isGreaterThan", {
    type: "isGreaterThan",
    component: SingleInputCriterionForm,
    category: "number",
    sequence: 50,
});
criterionComponentRegistry.add("isGreaterOrEqualTo", {
    type: "isGreaterOrEqualTo",
    component: SingleInputCriterionForm,
    category: "number",
    sequence: 60,
});
criterionComponentRegistry.add("isLessThan", {
    type: "isLessThan",
    component: SingleInputCriterionForm,
    category: "number",
    sequence: 30,
});
criterionComponentRegistry.add("isLessOrEqualTo", {
    type: "isLessOrEqualTo",
    component: SingleInputCriterionForm,
    category: "number",
    sequence: 40,
});
criterionComponentRegistry.add("isBetween", {
    type: "isBetween",
    component: DoubleInputCriterionForm,
    category: "number",
    sequence: 70,
});
criterionComponentRegistry.add("isNotBetween", {
    type: "isNotBetween",
    component: DoubleInputCriterionForm,
    category: "number",
    sequence: 80,
});
criterionComponentRegistry.add("isBoolean", {
    type: "isBoolean",
    component: undefined,
    category: "misc",
    sequence: 10,
});
criterionComponentRegistry.add("isValueInList", {
    type: "isValueInList",
    component: ListCriterionForm,
    category: "list",
    sequence: 10,
});
criterionComponentRegistry.add("isValueInRange", {
    type: "isValueInRange",
    component: ValueInRangeCriterionForm,
    category: "list",
    sequence: 20,
});
criterionComponentRegistry.add("customFormula", {
    type: "customFormula",
    component: SingleInputCriterionForm,
    category: "misc",
    sequence: 20,
});
criterionComponentRegistry.add("isEmpty", {
    type: "isEmpty",
    component: undefined,
    category: "misc",
    sequence: 5,
});
criterionComponentRegistry.add("isNotEmpty", {
    type: "isNotEmpty",
    component: undefined,
    category: "misc",
    sequence: 6,
});
function getCriterionMenuItems(callback, availableTypes) {
    const items = criterionComponentRegistry
        .getAll()
        .filter((item) => availableTypes.has(item.type))
        .sort((a, b) => {
        if (a.category === b.category) {
            return a.sequence - b.sequence;
        }
        return criterionCategoriesSequences[a.category] - criterionCategoriesSequences[b.category];
    });
    const actionSpecs = items.map((item, index) => {
        const evaluator = criterionEvaluatorRegistry.get(item.type);
        return {
            name: evaluator.name,
            id: item.type,
            separator: item.category !== items[index + 1]?.category,
            execute: () => callback(item.type),
        };
    });
    return createActions(actionSpecs);
}

/** This component looks like a select input, but on click it opens a MenuPopover with the items given as props instead of a dropdown */
class SelectMenu extends Component {
    static template = "o-spreadsheet-SelectMenu";
    static props = {
        menuItems: Array,
        selectedValue: String,
        class: { type: String, optional: true },
    };
    static components = { MenuPopover };
    menuId = new UuidGenerator().uuidv4();
    selectRef = useRef("select");
    state = useState({
        isMenuOpen: false,
    });
    onClick(ev) {
        if (ev.closedMenuId === this.menuId) {
            return;
        }
        this.state.isMenuOpen = !this.state.isMenuOpen;
    }
    onMenuClosed() {
        this.state.isMenuOpen = false;
    }
    get menuAnchorRect() {
        return getRefBoundingRect(this.selectRef);
    }
}

class FilterMenuCriterion extends Component {
    static template = "o-spreadsheet-FilterMenuCriterion";
    static props = {
        filterPosition: Object,
        onCriterionChanged: Function,
        criterionOperators: Array,
    };
    static components = { SelectMenu };
    state;
    setup() {
        onWillUpdateProps((nextProps) => {
            if (!deepEquals(nextProps.filterPosition, this.props.filterPosition)) {
                this.state.criterion = this.getFilterCriterionValue(nextProps.filterPosition);
            }
        });
        this.state = useState({
            criterion: this.getFilterCriterionValue(this.props.filterPosition),
        });
    }
    getFilterCriterionValue(position) {
        const sheetId = this.env.model.getters.getActiveSheetId();
        const filterValue = this.env.model.getters.getFilterCriterionValue({ sheetId, ...position });
        return filterValue?.filterType === "criterion"
            ? deepCopy(filterValue)
            : { filterType: "criterion", type: "none", values: [] };
    }
    get criterionMenuItems() {
        const noCriterionMenuItem = createAction({
            name: _t("None"),
            id: "none",
            separator: true,
            execute: () => this.onCriterionTypeChange("none"),
        });
        return [
            noCriterionMenuItem,
            ...getCriterionMenuItems((type) => this.onCriterionTypeChange(type), new Set(this.props.criterionOperators)),
        ];
    }
    get selectedCriterionName() {
        return this.state.criterion.type === "none"
            ? _t("None")
            : criterionEvaluatorRegistry.get(this.state.criterion.type).name;
    }
    get criterionComponent() {
        return this.state.criterion.type === "none"
            ? undefined
            : criterionComponentRegistry.get(this.state.criterion.type).component;
    }
    onCriterionChanged(criterion) {
        this.state.criterion.values = criterion.values;
        this.state.criterion.dateValue = criterion.dateValue;
        this.props.onCriterionChanged(this.state.criterion);
    }
    onCriterionTypeChange(type) {
        this.state.criterion.type = type;
        this.props.onCriterionChanged(this.state.criterion);
    }
}

const CHECK_SVG = /*xml*/ `
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'>
  <path fill='none' stroke='#FFF' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/>
</svg>
`;
const CHECKBOX_WIDTH = 14;
css /* scss */ `
  label.o-checkbox {
    input {
      appearance: none;
      -webkit-appearance: none;
      -moz-appearance: none;
      border-radius: 0;
      width: ${CHECKBOX_WIDTH}px;
      height: ${CHECKBOX_WIDTH}px;
      vertical-align: top;
      outline: none;
      border: 1px solid ${GRAY_300};
      cursor: pointer;

      &:hover {
        border-color: ${ACTION_COLOR};
      }

      &:checked {
        background: url("data:image/svg+xml,${encodeURIComponent(CHECK_SVG)}");
        background-color: ${ACTION_COLOR};
        border-color: ${ACTION_COLOR};
      }

      &:focus {
        outline: none;
        box-shadow: 0 0 0 0.25rem rgba(113, 75, 103, 0.25);
        border-color: ${ACTION_COLOR};
      }
    }
  }
`;
class Checkbox extends Component {
    static template = "o-spreadsheet.Checkbox";
    static props = {
        label: { type: String, optional: true },
        value: { type: Boolean, optional: true },
        className: { type: String, optional: true },
        name: { type: String, optional: true },
        title: { type: String, optional: true },
        disabled: { type: Boolean, optional: true },
        onChange: Function,
    };
    static defaultProps = { value: false };
    onChange(ev) {
        const value = ev.target.checked;
        this.props.onChange(value);
    }
}

class FilterMenuValueItem extends Component {
    static template = "o-spreadsheet-FilterMenuValueItem";
    static components = { Checkbox };
    static props = {
        value: String,
        isChecked: Boolean,
        isSelected: Boolean,
        onMouseMove: Function,
        onClick: Function,
        scrolledTo: { type: String, optional: true },
    };
    itemRef = useRef("menuValueItem");
    setup() {
        onWillPatch(() => {
            if (this.props.scrolledTo) {
                this.scrollListToSelectedValue();
            }
        });
    }
    scrollListToSelectedValue() {
        if (!this.itemRef.el) {
            return;
        }
        this.itemRef.el.scrollIntoView?.({
            block: this.props.scrolledTo === "bottom" ? "end" : "start",
        });
    }
}

class FilterMenuValueList extends Component {
    static template = "o-spreadsheet-FilterMenuValueList";
    static props = {
        filterPosition: Object,
        onUpdateHiddenValues: Function,
    };
    static components = { FilterMenuValueItem };
    state = useState({
        values: [],
        displayedValues: [],
        textFilter: "",
        selectedValue: undefined,
        numberOfDisplayedValues: 50,
        hasMoreValues: false,
    });
    searchBar = useRef("filterMenuSearchBar");
    setup() {
        onWillUpdateProps((nextProps) => {
            if (!deepEquals(nextProps.filterPosition, this.props.filterPosition)) {
                this.state.values = this.getFilterHiddenValues(nextProps.filterPosition);
                this.computeDisplayedValues();
            }
        });
        this.state.values = this.getFilterHiddenValues(this.props.filterPosition);
        this.computeDisplayedValues();
    }
    getFilterHiddenValues(position) {
        const sheetId = this.env.model.getters.getActiveSheetId();
        const filter = this.env.model.getters.getFilter({ sheetId, ...position });
        if (!filter) {
            return [];
        }
        const filterValue = this.env.model.getters.getFilterValue({ sheetId, ...position });
        let cells = (filter.filteredRange ? positions(filter.filteredRange.zone) : []).map((position) => ({
            position,
            cellValue: this.env.model.getters.getEvaluatedCell({ sheetId, ...position }).formattedValue,
        }));
        if (filterValue?.filterType !== "criterion") {
            cells = cells.filter((val) => !this.env.model.getters.isRowHidden(sheetId, val.position.row));
        }
        const cellValues = cells.map((val) => val.cellValue);
        const filterValues = filterValue?.filterType === "values" ? filterValue.hiddenValues : [];
        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: filterValue?.filterType !== "criterion"
                        ? !normalizedFilteredValues.has(normalizedValue)
                        : false,
                    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) {
        this.state.selectedValue = value.string;
        value.checked = !value.checked;
        this.searchBar.el?.focus();
        this.updateHiddenValues();
    }
    onMouseMove(value) {
        this.state.selectedValue = value.string;
    }
    selectAll() {
        this.state.displayedValues.forEach((value) => (value.checked = true));
        this.props.onUpdateHiddenValues([]);
    }
    clearAll() {
        this.state.displayedValues.forEach((value) => (value.checked = false));
        const hiddenValues = this.state.values.map((val) => val.string);
        this.props.onUpdateHiddenValues(hiddenValues);
    }
    updateHiddenValues() {
        const hiddenValues = this.state.values.filter((val) => !val.checked).map((val) => val.string);
        this.props.onUpdateHiddenValues(hiddenValues);
    }
    updateSearch(ev) {
        const target = ev.target;
        this.state.textFilter = target.value;
        this.state.selectedValue = undefined;
        this.computeDisplayedValues();
    }
    computeDisplayedValues() {
        const values = !this.state.textFilter
            ? this.state.values
            : fuzzyLookup(this.state.textFilter, this.state.values, (val) => val.string);
        this.state.displayedValues = values.slice(0, this.state.numberOfDisplayedValues);
        this.state.hasMoreValues = values.length > this.state.numberOfDisplayedValues;
    }
    loadMoreValues() {
        this.state.numberOfDisplayedValues += 100;
        this.computeDisplayedValues();
    }
    onKeyDown(ev) {
        const displayedValues = this.state.displayedValues;
        if (displayedValues.length === 0)
            return;
        let selectedIndex = undefined;
        if (this.state.selectedValue !== undefined) {
            const index = displayedValues.findIndex((val) => val.string === this.state.selectedValue);
            selectedIndex = index === -1 ? undefined : index;
        }
        switch (ev.key) {
            case "ArrowDown":
                if (selectedIndex === undefined) {
                    selectedIndex = 0;
                }
                else {
                    selectedIndex = Math.min(selectedIndex + 1, displayedValues.length - 1);
                }
                ev.preventDefault();
                ev.stopPropagation();
                break;
            case "ArrowUp":
                if (selectedIndex === undefined) {
                    selectedIndex = displayedValues.length - 1;
                }
                else {
                    selectedIndex = Math.max(selectedIndex - 1, 0);
                }
                ev.preventDefault();
                ev.stopPropagation();
                break;
            case "Enter":
                if (selectedIndex !== undefined) {
                    this.checkValue(displayedValues[selectedIndex]);
                }
                ev.stopPropagation();
                ev.preventDefault();
                break;
        }
        this.state.selectedValue =
            selectedIndex !== undefined ? displayedValues[selectedIndex].string : undefined;
        if (ev.key === "ArrowUp" || ev.key === "ArrowDown") {
            this.scrollListToSelectedValue(ev.key);
        }
    }
    clearScrolledToValue() {
        this.state.values.forEach((val) => (val.scrolledTo = undefined));
    }
    scrollListToSelectedValue(arrow) {
        this.clearScrolledToValue();
        const selectedValue = this.state.values.find((val) => val.string === this.state.selectedValue);
        if (selectedValue) {
            selectedValue.scrolledTo = arrow === "ArrowUp" ? "top" : "bottom";
        }
    }
}

css /* scss */ `
  .o-filter-menu {
    width: 245px;
    padding: 8px 0;
    user-select: none;

    .o-filter-menu-content {
      padding: 0 16px;
    }

    .o-sort-item {
      padding-left: 34px;
    }

    .o_side_panel_collapsible_title {
      font-size: inherit;
      padding: 0 0 4px 0 !important;
      font-weight: 400 !important;

      .collapsor .o-icon {
        opacity: 0.8;
      }

      .collapsor-arrow {
        transform-origin: 6px 8px;

        .o-icon {
          width: 12px;
          height: 16px;
        }
      }
    }

    .o-filter-menu-item {
      display: flex;
      cursor: pointer;
      user-select: none;
      line-height: 1;

      &.selected,
      &:hover {
        background-color: ${BUTTON_ACTIVE_BG};
      }
    }

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

      .o-button {
        height: 26px;
      }
    }
  }
`;
class FilterMenu extends Component {
    static template = "o-spreadsheet-FilterMenu";
    static props = {
        filterPosition: Object,
        onClosed: { type: Function, optional: true },
    };
    static components = { FilterMenuValueList, SidePanelCollapsible, FilterMenuCriterion };
    criterionCategory = "text";
    updatedCriterionValue;
    setup() {
        onWillUpdateProps((nextProps) => {
            if (!deepEquals(nextProps.filterPosition, this.props.filterPosition)) {
                this.updatedCriterionValue = undefined;
                this.criterionCategory = this.getCriterionCategory(nextProps.filterPosition);
            }
        });
        this.criterionCategory = this.getCriterionCategory(this.props.filterPosition);
    }
    get isSortable() {
        if (!this.table) {
            return false;
        }
        const coreTable = this.env.model.getters.getCoreTableMatchingTopLeft(this.table.range.sheetId, this.table.range.zone);
        return !this.env.model.getters.isReadonly() && coreTable?.type !== "dynamic";
    }
    get table() {
        const sheetId = this.env.model.getters.getActiveSheetId();
        const position = this.props.filterPosition;
        return this.env.model.getters.getTable({ sheetId, ...position });
    }
    get filterValueType() {
        const sheetId = this.env.model.getters.getActiveSheetId();
        const position = this.props.filterPosition;
        const filterValue = this.env.model.getters.getFilterValue({ sheetId, ...position });
        return filterValue?.filterType;
    }
    getCriterionCategory(position) {
        const sheetId = this.env.model.getters.getActiveSheetId();
        const filter = this.env.model.getters.getFilter({ sheetId, ...position });
        if (!filter || !filter.filteredRange) {
            return "text";
        }
        const cellTypesCount = { text: 0, number: 0, date: 0 };
        const filteredZone = filter.filteredRange.zone;
        for (let row = filteredZone.top; row <= filteredZone.bottom; row++) {
            // 100 rows should be enough to determine the type, let's not loop on 10,000 rows for nothing
            if (row > 100) {
                break;
            }
            const cell = this.env.model.getters.getEvaluatedCell({ sheetId, row, col: position.col });
            if (cell.type === CellValueType.text || cell.type === CellValueType.boolean) {
                cellTypesCount.text++;
            }
            else if (cell.type === CellValueType.number) {
                if (cell.format && isDateTimeFormat(cell.format)) {
                    cellTypesCount.date++;
                }
                else {
                    cellTypesCount.number++;
                }
            }
        }
        const max = Math.max(cellTypesCount.text, cellTypesCount.number, cellTypesCount.date);
        const type = Object.keys(cellTypesCount).find((key) => cellTypesCount[key] === max);
        return (type || "text");
    }
    onUpdateHiddenValues(values) {
        this.updatedCriterionValue = { filterType: "values", hiddenValues: values };
    }
    onCriterionChanged(criterion) {
        this.updatedCriterionValue = criterion;
    }
    confirm() {
        if (!this.updatedCriterionValue) {
            this.props.onClosed?.();
            return;
        }
        const position = this.props.filterPosition;
        this.env.model.dispatch("UPDATE_FILTER", {
            ...position,
            sheetId: this.env.model.getters.getActiveSheetId(),
            value: this.updatedCriterionValue,
        });
        this.props.onClosed?.();
    }
    get criterionOperators() {
        if (this.criterionCategory === "date") {
            return filterDateCriterionOperators;
        }
        else if (this.criterionCategory === "number") {
            return filterNumberCriterionOperators;
        }
        return filterTextCriterionOperators;
    }
    cancel() {
        this.props.onClosed?.();
    }
    sortFilterZone(sortDirection) {
        const filterPosition = this.props.filterPosition;
        const table = this.table;
        const tableZone = table?.range.zone;
        if (!filterPosition || !tableZone || tableZone.top === tableZone.bottom) {
            return;
        }
        const sheetId = this.env.model.getters.getActiveSheetId();
        const contentZone = { ...tableZone, top: tableZone.top + 1 };
        const sortAnchor = { col: filterPosition.col, row: contentZone.top };
        const sortOptions = { emptyCellAsZero: true, sortHeaders: true };
        interactiveSort(this.env, sheetId, sortAnchor, contentZone, sortDirection, sortOptions);
        this.props.onClosed?.();
    }
}
const FilterMenuPopoverBuilder = {
    onOpen: (position, getters) => {
        return {
            isOpen: true,
            props: { filterPosition: position },
            Component: FilterMenu,
            cellCorner: "bottom-left",
        };
    },
};

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

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

    a.o-link {
      color: ${LINK_COLOR};
      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 Component {
    static template = "o-spreadsheet-LinkDisplay";
    static props = {
        cellPosition: Object,
        onClosed: { type: Function, optional: true },
    };
    cellPopovers;
    setup() {
        this.cellPopovers = useStore(CellPopoverStore);
    }
    get cell() {
        const { col, row } = this.props.cellPosition;
        const sheetId = this.env.model.getters.getActiveSheetId();
        return this.env.model.getters.getEvaluatedCell({ sheetId, col, row });
    }
    get link() {
        if (this.cell.link) {
            return this.cell.link;
        }
        const { col, row } = this.props.cellPosition;
        throw new Error(`LinkDisplay Component can only be used with link cells. ${toXC(col, row)} is not a link.`);
    }
    getUrlRepresentation(link) {
        return urlRepresentation(link, this.env.model.getters);
    }
    openLink(ev) {
        openLink(this.link, this.env, isMiddleClickOrCtrlClick(ev));
    }
    edit() {
        const { col, row } = this.props.cellPosition;
        this.env.model.selection.selectCell(col, row);
        this.cellPopovers.open({ col, row }, "LinkEditor");
    }
    unlink() {
        const sheetId = this.env.model.getters.getActiveSheetId();
        const { col, row } = this.props.cellPosition;
        const style = this.env.model.getters.getCellComputedStyle({ sheetId, col, row });
        const textColor = style?.textColor === LINK_COLOR ? undefined : style?.textColor;
        this.env.model.dispatch("UPDATE_CELL", {
            col,
            row,
            sheetId,
            content: this.link.label,
            style: { ...style, textColor, underline: undefined },
        });
    }
}
const LinkCellPopoverBuilder = {
    onHover: (position, getters) => {
        const cell = getters.getEvaluatedCell(position);
        const shouldDisplayLink = !getters.isDashboard() && cell.link && getters.isVisibleInViewport(position);
        if (!shouldDisplayLink)
            return { isOpen: false };
        return {
            isOpen: true,
            Component: LinkDisplay,
            props: { cellPosition: position },
            cellCorner: "bottom-left",
        };
    },
};

const linkSheet = {
    name: _t("Link sheet"),
    children: [
        (env) => {
            const sheets = env.model.getters
                .getSheetIds()
                .map((sheetId) => env.model.getters.getSheet(sheetId));
            return sheets.map((sheet) => ({
                id: sheet.id,
                name: sheet.name,
                execute: () => markdownLink(sheet.name, buildSheetLink(sheet.id)),
            }));
        },
    ],
};
const deleteSheet = {
    name: _t("Delete"),
    isVisible: (env) => {
        return env.model.getters.getVisibleSheetIds().length > 1;
    },
    execute: (env) => env.askConfirmation(_t("Are you sure you want to delete this sheet?"), () => {
        env.model.dispatch("DELETE_SHEET", {
            sheetId: env.model.getters.getActiveSheetId(),
            sheetName: env.model.getters.getActiveSheetName(),
        });
    }),
    icon: "o-spreadsheet-Icon.TRASH",
};
const duplicateSheet = {
    name: _t("Duplicate"),
    execute: (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 });
    },
    icon: "o-spreadsheet-Icon.COPY",
};
const renameSheet = (args) => {
    return {
        name: _t("Rename"),
        execute: args.renameSheetCallback,
        icon: "o-spreadsheet-Icon.RENAME_SHEET",
    };
};
const changeSheetColor = (args) => {
    return {
        name: _t("Change color"),
        execute: args.openSheetColorPickerCallback,
        icon: "o-spreadsheet-Icon.PAINT_FORMAT",
    };
};
const sheetMoveRight = {
    name: _t("Move right"),
    isVisible: (env) => {
        const sheetId = env.model.getters.getActiveSheetId();
        const sheetIds = env.model.getters.getVisibleSheetIds();
        return sheetIds.indexOf(sheetId) !== sheetIds.length - 1;
    },
    execute: (env) => env.model.dispatch("MOVE_SHEET", {
        sheetId: env.model.getters.getActiveSheetId(),
        delta: 1,
    }),
    icon: "o-spreadsheet-Icon.MOVE_SHEET_RIGHT",
};
const sheetMoveLeft = {
    name: _t("Move left"),
    isVisible: (env) => {
        const sheetId = env.model.getters.getActiveSheetId();
        return env.model.getters.getVisibleSheetIds()[0] !== sheetId;
    },
    execute: (env) => env.model.dispatch("MOVE_SHEET", {
        sheetId: env.model.getters.getActiveSheetId(),
        delta: -1,
    }),
    icon: "o-spreadsheet-Icon.MOVE_SHEET_LEFT",
};
const hideSheet = {
    name: _t("Hide sheet"),
    isVisible: (env) => env.model.getters.getVisibleSheetIds().length !== 1,
    execute: (env) => env.model.dispatch("HIDE_SHEET", { sheetId: env.model.getters.getActiveSheetId() }),
    icon: "o-spreadsheet-Icon.HIDE_SHEET",
};

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

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

const PADDING = 12;
const LINK_EDITOR_WIDTH = 340 + 2 * PADDING;
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;
    width: ${LINK_EDITOR_WIDTH}px;

    .o-section {
      .o-section-title {
        font-weight: bold;
        margin-bottom: 5px;
      }
    }
    .o-buttons {
      padding-left: 16px;
      padding-top: 16px;
      text-align: right;
    }
    input.o-input {
      width: 100%;
      padding: 0 23px 4px 0;
    }
    .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 Component {
    static template = "o-spreadsheet-LinkEditor";
    static props = {
        cellPosition: Object,
        onClosed: { type: Function, optional: true },
    };
    static components = { MenuPopover };
    menuItems = linkMenuRegistry.getMenuItems();
    link = useState(this.defaultState);
    menu = useState({
        isOpen: false,
    });
    linkEditorMenuButtonRef = useRef("linkEditorMenuButton");
    urlInput = useRef("urlInput");
    setup() {
        onMounted(() => this.urlInput.el?.focus());
    }
    get defaultState() {
        const { col, row } = this.props.cellPosition;
        const sheetId = this.env.model.getters.getActiveSheetId();
        const cell = this.env.model.getters.getEvaluatedCell({ sheetId, col, row });
        if (cell.link) {
            return {
                url: cell.link.url,
                label: cell.formattedValue,
                isUrlEditable: cell.link.isUrlEditable,
            };
        }
        return {
            label: cell.formattedValue,
            url: "",
            isUrlEditable: true,
        };
    }
    get menuButtonRect() {
        return getRefBoundingRect(this.linkEditorMenuButtonRef);
    }
    onSpecialLink(ev) {
        const { detail: markdownLink } = ev;
        const link = detectLink(markdownLink);
        if (!link) {
            return;
        }
        this.link.url = link.url;
        this.link.label = link.label;
        this.link.isUrlEditable = link.isUrlEditable;
    }
    getUrlRepresentation(link) {
        return urlRepresentation(link, this.env.model.getters);
    }
    openMenu() {
        this.menu.isOpen = true;
    }
    removeLink() {
        this.link.url = "";
        this.link.isUrlEditable = true;
    }
    save() {
        const { col, row } = this.props.cellPosition;
        const locale = this.env.model.getters.getLocale();
        const label = this.link.label
            ? canonicalizeNumberContent(this.link.label, locale)
            : this.link.url;
        this.env.model.dispatch("UPDATE_CELL", {
            col: col,
            row: row,
            sheetId: this.env.model.getters.getActiveSheetId(),
            content: markdownLink(label, this.link.url),
        });
        this.props.onClosed?.();
    }
    cancel() {
        this.props.onClosed?.();
    }
    onKeyDown(ev) {
        switch (ev.key) {
            case "Enter":
                if (this.link.url) {
                    this.save();
                }
                ev.stopPropagation();
                ev.preventDefault();
                break;
            case "Escape":
                this.cancel();
                ev.stopPropagation();
                break;
        }
    }
}
const LinkEditorPopoverBuilder = {
    onOpen: (position, getters) => {
        return {
            isOpen: true,
            props: { cellPosition: position },
            Component: LinkEditor,
            cellCorner: "bottom-left",
        };
    },
};

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

const DEFAULT_BAR_CHART_CONFIG = {
    type: "bar",
    title: {},
    dataSets: [],
    legendPosition: "none",
    dataSetsHaveTitle: false,
    stacked: false,
};
const DEFAULT_LINE_CHART_CONFIG = {
    type: "line",
    title: {},
    dataSets: [],
    legendPosition: "none",
    dataSetsHaveTitle: false,
    stacked: false,
    cumulative: false,
    labelsAsText: false,
};
function getUnboundRange(getters, zone) {
    return zoneToXc(getters.getUnboundedZone(getters.getActiveSheetId(), zone));
}
function detectColumnType(cells) {
    if (!cells.length) {
        return "empty";
    }
    const counts = { number: 0, text: 0, date: 0, percentage: 0 };
    let max = 0;
    let detectedType = "empty";
    for (const cell of cells) {
        let type = null;
        if (cell.type === CellValueType.number) {
            if (cell.format && isDateTimeFormat(cell.format)) {
                type = "date";
            }
            else if (cell.format?.includes("%")) {
                type = "percentage";
            }
            else {
                type = "number";
            }
        }
        else if (cell.type === CellValueType.text) {
            type = "text";
        }
        if (type) {
            const newCount = ++counts[type];
            if (newCount > max) {
                max = newCount;
                detectedType = type;
            }
        }
    }
    return detectedType;
}
function categorizeColumns(zones, getters) {
    const columns = [];
    for (const zone of getZonesByColumns(zones)) {
        const cells = getters.getEvaluatedCellsInZone(getters.getActiveSheetId(), zone);
        columns.push({ zone, type: detectColumnType(cells) });
    }
    return columns;
}
function getCellStats(getters, zone) {
    const cells = getters.getEvaluatedCellsInZone(getters.getActiveSheetId(), zone);
    const values = cells.map((c) => c.value?.toString().trim() || "").filter((s) => s);
    return {
        uniqueCount: new Set(values).size,
        totalCount: values.length,
    };
}
function isDatasetTitled(getters, column) {
    const titleCell = getters.getEvaluatedCell({
        sheetId: getters.getActiveSheetId(),
        col: column.zone.left,
        row: column.zone.top,
    });
    return ![CellValueType.number, CellValueType.empty].includes(titleCell.type);
}
/**
 * Builds a chart definition for a single column selection. The logic to detect the chart type is as follows:
 * - If the column contains a single cell, create a scorecard.
 * - If the column type is "percentage", create a pie chart.
 * - If the column type is "text", create a pie chart
 * - If the column type is "date", create a line chart.
 * - Otherwise, create a bar chart.
 */
function buildSingleColumnChart(column, getters) {
    const { type, zone } = column;
    const sheetId = getters.getActiveSheetId();
    const dataSetsHaveTitle = isDatasetTitled(getters, column);
    const dataRange = getUnboundRange(getters, zone);
    const titleCell = getters.getEvaluatedCell({ sheetId, col: zone.left, row: zone.top });
    if (getZoneArea(zone) === 1) {
        return buildScorecard(zone, getters);
    }
    switch (type) {
        case "percentage":
            return {
                type: "pie",
                title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
                dataSets: [{ dataRange }],
                legendPosition: "none",
                dataSetsHaveTitle,
            };
        case "text":
            const cells = getters.getEvaluatedCellsInZone(sheetId, zone);
            const titleCount = cells.reduce((count, cell) => (cell.value === titleCell.value ? count + 1 : count), 0);
            const hasUniqueTitle = titleCell.value !== null && titleCount === 1;
            return {
                type: "pie",
                title: hasUniqueTitle ? { text: String(titleCell.value) } : {},
                dataSets: [{ dataRange }],
                labelRange: dataRange,
                dataSetsHaveTitle: hasUniqueTitle,
                aggregated: true,
                legendPosition: "top",
            };
        case "date":
            return {
                ...DEFAULT_LINE_CHART_CONFIG,
                type: "line",
                title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
                dataSets: [{ dataRange }],
                dataSetsHaveTitle,
            };
    }
    return {
        ...DEFAULT_BAR_CHART_CONFIG,
        title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
        dataSets: [{ dataRange }],
        dataSetsHaveTitle,
    };
}
/**
 * Builds a chart definition for a selection of two columns. The logic to detect the chart type always consider the
 * columns left to right, and is as follows:
 * - any type + percentage columns: pie chart
 * - number + number columns: scatter chart
 * - date + number columns: line chart
 * - text + number columns: treemap if repetition in labels
 * - any other combination: bar chart
 */
function buildTwoColumnChart(columns, getters) {
    if (columns.length !== 2) {
        throw new Error("buildTwoColumnChart expects exactly two columns");
    }
    if (columns[1].type === "percentage") {
        return {
            type: "pie",
            title: {},
            dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }],
            labelRange: getUnboundRange(getters, columns[0].zone),
            dataSetsHaveTitle: isDatasetTitled(getters, columns[1]),
            aggregated: true,
            legendPosition: "none",
        };
    }
    if (columns[0].type === "number" && columns[1].type === "number") {
        return {
            type: "scatter",
            title: {},
            dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }],
            labelRange: getUnboundRange(getters, columns[0].zone),
            dataSetsHaveTitle: isDatasetTitled(getters, columns[1]),
            labelsAsText: false,
            legendPosition: "none",
        };
    }
    // TODO: Handle date + number with calendar chart when implemented (and change the docstring)
    if (columns[0].type === "date" && columns[1].type === "number") {
        return {
            ...DEFAULT_LINE_CHART_CONFIG,
            type: "line",
            dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }],
            labelRange: getUnboundRange(getters, columns[0].zone),
            dataSetsHaveTitle: isDatasetTitled(getters, columns[0]),
        };
    }
    if (columns[0].type === "text" && columns[1].type === "number") {
        const textColumn = columns[0];
        const numberColumn = columns[1];
        const { uniqueCount, totalCount } = getCellStats(getters, textColumn.zone);
        const dataSetsHaveTitle = isDatasetTitled(getters, numberColumn);
        if (uniqueCount !== totalCount) {
            return {
                type: "treemap",
                title: {},
                dataSets: [{ dataRange: getUnboundRange(getters, textColumn.zone) }],
                labelRange: getUnboundRange(getters, numberColumn.zone),
                dataSetsHaveTitle,
                legendPosition: "none",
            };
        }
    }
    return {
        ...DEFAULT_BAR_CHART_CONFIG,
        dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }],
        labelRange: getUnboundRange(getters, columns[0].zone),
        dataSetsHaveTitle: isDatasetTitled(getters, columns[1]),
    };
}
/**
 * Builds a chart definition for a selection more than two columns. The logic to detect the chart type always consider
 * the columns left to right, and is as follows:
 * - multiple text + single number/percentage columns: sunburst if 3+ text columns, treemap otherwise
 * - any type + multiple percentage columns: pie chart
 * - date + multiple number columns: line chart
 * - any other combination: bar chart
 */
function buildMultiColumnChart(columns, getters) {
    if (columns.length < 3) {
        throw new Error("buildMultiColumnChart expects at least three columns");
    }
    const dataSetsHaveTitle = columns.some((col) => col.type !== "text" && isDatasetTitled(getters, col));
    const lastColumn = columns[columns.length - 1];
    const columnsExceptLast = columns.slice(0, columns.length - 1);
    if ((lastColumn.type === "percentage" || lastColumn.type === "number") &&
        columnsExceptLast.every((col) => col.type === "text")) {
        const dataSets = columnsExceptLast.map(({ zone }) => ({
            dataRange: getUnboundRange(getters, zone),
        }));
        return {
            type: columnsExceptLast.length >= 3 ? "sunburst" : "treemap",
            title: {},
            dataSets,
            labelRange: getUnboundRange(getters, lastColumn.zone),
            dataSetsHaveTitle,
            legendPosition: "none",
        };
    }
    const firstColumn = columns[0];
    const columnsExceptFirst = columns.slice(1);
    const rangesOfColumnsExceptFirst = columnsExceptFirst.map(({ zone }) => ({
        dataRange: getUnboundRange(getters, zone),
    }));
    if (columnsExceptFirst.every((col) => col.type === "percentage")) {
        return {
            type: "pie",
            title: {},
            dataSets: rangesOfColumnsExceptFirst,
            labelRange: getUnboundRange(getters, firstColumn.zone),
            dataSetsHaveTitle,
            aggregated: false,
            legendPosition: "top",
        };
    }
    if (firstColumn.type === "date" && columnsExceptFirst.every((col) => col.type === "number")) {
        return {
            ...DEFAULT_LINE_CHART_CONFIG,
            type: "line",
            dataSets: rangesOfColumnsExceptFirst,
            labelRange: getUnboundRange(getters, firstColumn.zone),
            dataSetsHaveTitle,
            legendPosition: "top",
        };
    }
    return {
        ...DEFAULT_BAR_CHART_CONFIG,
        dataSets: rangesOfColumnsExceptFirst,
        labelRange: getUnboundRange(getters, firstColumn.zone),
        dataSetsHaveTitle,
        legendPosition: "top",
    };
}
function buildScorecard(zone, getters) {
    const cell = getters.getCell({
        sheetId: getters.getActiveSheetId(),
        col: zone.left,
        row: zone.top,
    });
    return {
        type: "scorecard",
        title: {},
        keyValue: getUnboundRange(getters, zone),
        background: cell?.style?.fillColor,
        baselineMode: DEFAULT_SCORECARD_BASELINE_MODE,
        baselineColorUp: DEFAULT_SCORECARD_BASELINE_COLOR_UP,
        baselineColorDown: DEFAULT_SCORECARD_BASELINE_COLOR_DOWN,
    };
}
/**
 * Analyzes selected zones and intelligently determines the most suitable chart.
 */
function getSmartChartDefinition(zones, getters) {
    const columns = categorizeColumns(zones, getters);
    if (columns.length === 0 || columns.every((col) => col.type === "empty")) {
        const dataSets = columns.map(({ zone }) => ({ dataRange: getUnboundRange(getters, zone) }));
        return { ...DEFAULT_BAR_CHART_CONFIG, dataSets };
    }
    const nonEmptyColumns = columns.filter((col) => col.type !== "empty");
    switch (nonEmptyColumns.length) {
        case 1:
            return buildSingleColumnChart(nonEmptyColumns[0], getters);
        case 2:
            return buildTwoColumnChart(nonEmptyColumns, getters);
        default:
            return buildMultiColumnChart(nonEmptyColumns, getters);
    }
}

const TABLE_STYLE_CATEGORIES = {
    light: _t("Light"),
    medium: _t("Medium"),
    dark: _t("Dark"),
    custom: _t("Custom"),
};
const DEFAULT_TABLE_CONFIG = {
    hasFilters: false,
    totalRow: false,
    firstColumn: false,
    lastColumn: false,
    numberOfHeaders: 1,
    bandedRows: true,
    bandedColumns: false,
    automaticAutofill: true,
    styleId: "TableStyleMedium2",
};
function generateTableColorSet(name, highlightColor) {
    return {
        coloredText: darkenColor(highlightColor, 0.3),
        light: lightenColor(highlightColor, 0.8),
        medium: lightenColor(highlightColor, 0.6),
        dark: darkenColor(highlightColor, 0.3),
        mediumBorder: lightenColor(highlightColor, 0.45),
        highlight: highlightColor,
        name,
    };
}
const COLOR_SETS = {
    black: {
        name: _t("Black"),
        coloredText: "#000000",
        light: "#D9D9D9",
        medium: "#A6A6A6",
        dark: "#404040",
        mediumBorder: "#000000",
        highlight: "#000000",
    },
    lightBlue: generateTableColorSet(_t("Light blue"), "#346B90"),
    red: generateTableColorSet(_t("Red"), "#C53628"),
    lightGreen: generateTableColorSet(_t("Light green"), "#748747"),
    purple: generateTableColorSet(_t("Purple"), "#6C4E65"),
    gray: {
        name: _t("Gray"),
        coloredText: "#666666",
        light: "#EEEEEE",
        medium: "#DDDDDD",
        dark: "#767676",
        mediumBorder: "#D0D0D0",
        highlight: "#A9A9A9",
    },
    orange: generateTableColorSet(_t("Orange"), "#C37034"),
};
const DARK_COLOR_SETS = {
    black: COLOR_SETS.black,
    orangeBlue: { ...COLOR_SETS.lightBlue, highlight: COLOR_SETS.orange.highlight },
    purpleGreen: { ...COLOR_SETS.lightGreen, highlight: COLOR_SETS.purple.highlight },
    redBlue: { ...COLOR_SETS.lightBlue, highlight: COLOR_SETS.red.highlight },
};
const lightColoredText = (colorSet) => ({
    category: "light",
    templateName: "lightColoredText",
    primaryColor: colorSet.highlight,
    wholeTable: {
        style: { textColor: colorSet.coloredText },
        border: {
            top: { color: colorSet.highlight, style: "thin" },
            bottom: { color: colorSet.highlight, style: "thin" },
        },
    },
    headerRow: { border: { bottom: { color: colorSet.highlight, style: "thin" } } },
    totalRow: { border: { top: { color: colorSet.highlight, style: "thin" } } },
    firstRowStripe: { style: { fillColor: colorSet.light } },
});
const lightWithHeader = (colorSet) => ({
    category: "light",
    templateName: "lightWithHeader",
    primaryColor: colorSet.highlight,
    wholeTable: {
        border: {
            top: { color: colorSet.highlight, style: "thin" },
            bottom: { color: colorSet.highlight, style: "thin" },
            left: { color: colorSet.highlight, style: "thin" },
            right: { color: colorSet.highlight, style: "thin" },
        },
    },
    headerRow: {
        style: { fillColor: colorSet.highlight, textColor: "#FFFFFF" },
        border: { bottom: { color: colorSet.highlight, style: "thin" } },
    },
    totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } }, // @compatibility: should be double line
    firstRowStripe: { border: { bottom: { color: colorSet.highlight, style: "thin" } } },
    secondRowStripe: { border: { bottom: { color: colorSet.highlight, style: "thin" } } },
});
const lightAllBorders = (colorSet) => ({
    category: "light",
    templateName: "lightAllBorders",
    primaryColor: colorSet.highlight,
    wholeTable: {
        border: {
            top: { color: colorSet.highlight, style: "thin" },
            bottom: { color: colorSet.highlight, style: "thin" },
            left: { color: colorSet.highlight, style: "thin" },
            right: { color: colorSet.highlight, style: "thin" },
            horizontal: { color: colorSet.highlight, style: "thin" },
            vertical: { color: colorSet.highlight, style: "thin" },
        },
    },
    headerRow: { border: { bottom: { color: colorSet.highlight, style: "medium" } } },
    totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } }, // @compatibility: should be double line
    firstRowStripe: { style: { fillColor: colorSet.light } },
    firstColumnStripe: { style: { fillColor: colorSet.light } },
});
const mediumBandedBorders = (colorSet) => ({
    category: "medium",
    templateName: "mediumBandedBorders",
    primaryColor: colorSet.highlight,
    wholeTable: {
        border: {
            top: { color: colorSet.mediumBorder, style: "thin" },
            bottom: { color: colorSet.mediumBorder, style: "thin" },
            left: { color: colorSet.mediumBorder, style: "thin" },
            right: { color: colorSet.mediumBorder, style: "thin" },
            horizontal: { color: colorSet.mediumBorder, style: "thin" },
        },
    },
    headerRow: {
        style: { fillColor: colorSet.highlight, textColor: "#FFFFFF" },
    },
    totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } }, // @compatibility: should be double line
    firstRowStripe: { style: { fillColor: colorSet.light } },
    firstColumnStripe: { style: { fillColor: colorSet.light } },
});
const mediumWhiteBorders = (colorSet) => ({
    category: "medium",
    templateName: "mediumWhiteBorders",
    primaryColor: colorSet.highlight,
    wholeTable: {
        border: {
            horizontal: { color: "#FFFFFF", style: "thin" },
            vertical: { color: "#FFFFFF", style: "thin" },
        },
        style: { fillColor: colorSet.light },
    },
    headerRow: {
        border: { bottom: { color: "#FFFFFF", style: "thick" } },
        style: { fillColor: colorSet.highlight, textColor: "#FFFFFF" },
    },
    totalRow: {
        border: { top: { color: "#FFFFFF", style: "thick" } },
        style: { fillColor: colorSet.highlight, textColor: "#FFFFFF" },
    },
    firstColumn: { style: { fillColor: colorSet.highlight, textColor: "#FFFFFF" } },
    lastColumn: { style: { fillColor: colorSet.highlight, textColor: "#FFFFFF" } },
    firstRowStripe: { style: { fillColor: colorSet.medium } },
    firstColumnStripe: { style: { fillColor: colorSet.medium } },
});
const mediumMinimalBorders = (colorSet) => ({
    category: "medium",
    templateName: "mediumMinimalBorders",
    primaryColor: colorSet.highlight,
    wholeTable: {
        border: {
            top: { color: "#000000", style: "medium" },
            bottom: { color: "#000000", style: "medium" },
        },
    },
    totalRow: { border: { top: { color: "#000000", style: "medium" } } }, // @compatibility: should be double line
    headerRow: {
        style: { fillColor: colorSet.highlight, textColor: "#FFFFFF" },
        border: { bottom: { color: "#000000", style: "medium" } },
    },
    firstColumn: { style: { fillColor: colorSet.highlight, textColor: "#FFFFFF" } },
    lastColumn: { style: { fillColor: colorSet.highlight, textColor: "#FFFFFF" } },
    firstRowStripe: { style: { fillColor: COLOR_SETS.black.light } },
    firstColumnStripe: { style: { fillColor: COLOR_SETS.black.light } },
});
const mediumAllBorders = (colorSet) => ({
    category: "medium",
    templateName: "mediumAllBorders",
    primaryColor: colorSet.highlight,
    wholeTable: {
        border: {
            top: { color: colorSet.mediumBorder, style: "thin" },
            bottom: { color: colorSet.mediumBorder, style: "thin" },
            left: { color: colorSet.mediumBorder, style: "thin" },
            right: { color: colorSet.mediumBorder, style: "thin" },
            horizontal: { color: colorSet.mediumBorder, style: "thin" },
            vertical: { color: colorSet.mediumBorder, style: "thin" },
        },
        style: { fillColor: colorSet.light },
    },
    totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } }, // @compatibility: should be double line
    firstRowStripe: { style: { fillColor: colorSet.medium } },
    firstColumnStripe: { style: { fillColor: colorSet.medium } },
});
const dark = (colorSet) => ({
    category: "dark",
    templateName: "dark",
    primaryColor: colorSet.highlight,
    wholeTable: { style: { fillColor: colorSet.highlight, textColor: "#FFFFFF" } },
    totalRow: {
        style: { fillColor: colorSet.dark, textColor: "#FFFFFF" },
        border: { top: { color: "#FFFFFF", style: "thick" } },
    },
    headerRow: {
        style: { fillColor: "#000000" },
        border: { bottom: { color: "#FFFFFF", style: "thick" } },
    },
    firstColumn: {
        style: { fillColor: colorSet.dark },
        border: { right: { color: "#FFFFFF", style: "thick" } },
    },
    lastColumn: {
        style: { fillColor: colorSet.dark },
        border: { left: { color: "#FFFFFF", style: "thick" } },
    },
    firstRowStripe: { style: { fillColor: colorSet.dark } },
    firstColumnStripe: { style: { fillColor: colorSet.dark } },
});
const darkNoBorders = (colorSet) => ({
    category: "dark",
    templateName: "darkNoBorders",
    primaryColor: colorSet.highlight,
    wholeTable: { style: { fillColor: colorSet.light } },
    totalRow: { border: { top: { color: "#000000", style: "medium" } } }, // @compatibility: should be double line
    headerRow: { style: { fillColor: colorSet.highlight, textColor: "#FFFFFF" } },
    firstRowStripe: { style: { fillColor: colorSet.medium } },
    firstColumnStripe: { style: { fillColor: colorSet.medium } },
});
const darkTemplateInBlack = dark(COLOR_SETS.black);
darkTemplateInBlack.wholeTable.style.fillColor = "#737373";
const mediumMinimalBordersInBlack = mediumMinimalBorders(COLOR_SETS.black);
mediumMinimalBordersInBlack.wholeTable.border = {
    ...mediumMinimalBordersInBlack.wholeTable.border,
    left: { color: "#000000", style: "thin" },
    right: { color: "#000000", style: "thin" },
    horizontal: { color: "#000000", style: "thin" },
    vertical: { color: "#000000", style: "thin" },
};
function buildPreset(name, template, colorSet) {
    return { ...template(colorSet), displayName: `${colorSet.name}, ${name}` };
}
const TABLE_PRESETS = {
    None: { category: "light", templateName: "none", primaryColor: "", displayName: "none" },
    TableStyleLight1: buildPreset("TableStyleLight1", lightColoredText, COLOR_SETS.black),
    TableStyleLight2: buildPreset("TableStyleLight2", lightColoredText, COLOR_SETS.lightBlue),
    TableStyleLight3: buildPreset("TableStyleLight3", lightColoredText, COLOR_SETS.red),
    TableStyleLight4: buildPreset("TableStyleLight4", lightColoredText, COLOR_SETS.lightGreen),
    TableStyleLight5: buildPreset("TableStyleLight5", lightColoredText, COLOR_SETS.purple),
    TableStyleLight6: buildPreset("TableStyleLight6", lightColoredText, COLOR_SETS.gray),
    TableStyleLight7: buildPreset("TableStyleLight7", lightColoredText, COLOR_SETS.orange),
    TableStyleLight8: buildPreset("TableStyleLight8", lightWithHeader, COLOR_SETS.black),
    TableStyleLight9: buildPreset("TableStyleLight9", lightWithHeader, COLOR_SETS.lightBlue),
    TableStyleLight10: buildPreset("TableStyleLight10", lightWithHeader, COLOR_SETS.red),
    TableStyleLight11: buildPreset("TableStyleLight11", lightWithHeader, COLOR_SETS.lightGreen),
    TableStyleLight12: buildPreset("TableStyleLight12", lightWithHeader, COLOR_SETS.purple),
    TableStyleLight13: buildPreset("TableStyleLight13", lightWithHeader, COLOR_SETS.gray),
    TableStyleLight14: buildPreset("TableStyleLight14", lightWithHeader, COLOR_SETS.orange),
    TableStyleLight15: buildPreset("TableStyleLight15", lightAllBorders, COLOR_SETS.black),
    TableStyleLight16: buildPreset("TableStyleLight16", lightAllBorders, COLOR_SETS.lightBlue),
    TableStyleLight17: buildPreset("TableStyleLight17", lightAllBorders, COLOR_SETS.red),
    TableStyleLight18: buildPreset("TableStyleLight18", lightAllBorders, COLOR_SETS.lightGreen),
    TableStyleLight19: buildPreset("TableStyleLight19", lightAllBorders, COLOR_SETS.purple),
    TableStyleLight20: buildPreset("TableStyleLight20", lightAllBorders, COLOR_SETS.gray),
    TableStyleLight21: buildPreset("TableStyleLight21", lightAllBorders, COLOR_SETS.orange),
    TableStyleMedium1: buildPreset("TableStyleMedium1", mediumBandedBorders, COLOR_SETS.black),
    TableStyleMedium2: buildPreset("TableStyleMedium2", mediumBandedBorders, COLOR_SETS.lightBlue),
    TableStyleMedium3: buildPreset("TableStyleMedium3", mediumBandedBorders, COLOR_SETS.red),
    TableStyleMedium4: buildPreset("TableStyleMedium4", mediumBandedBorders, COLOR_SETS.lightGreen),
    TableStyleMedium5: buildPreset("TableStyleMedium5", mediumBandedBorders, COLOR_SETS.purple),
    TableStyleMedium6: buildPreset("TableStyleMedium6", mediumBandedBorders, COLOR_SETS.gray),
    TableStyleMedium7: buildPreset("TableStyleMedium7", mediumBandedBorders, COLOR_SETS.orange),
    TableStyleMedium8: buildPreset("TableStyleMedium8", mediumWhiteBorders, COLOR_SETS.black),
    TableStyleMedium9: buildPreset("TableStyleMedium9", mediumWhiteBorders, COLOR_SETS.lightBlue),
    TableStyleMedium10: buildPreset("TableStyleMedium10", mediumWhiteBorders, COLOR_SETS.red),
    TableStyleMedium11: buildPreset("TableStyleMedium11", mediumWhiteBorders, COLOR_SETS.lightGreen),
    TableStyleMedium12: buildPreset("TableStyleMedium12", mediumWhiteBorders, COLOR_SETS.purple),
    TableStyleMedium13: buildPreset("TableStyleMedium13", mediumWhiteBorders, COLOR_SETS.gray),
    TableStyleMedium14: buildPreset("TableStyleMedium14", mediumWhiteBorders, COLOR_SETS.orange),
    TableStyleMedium15: { ...mediumMinimalBordersInBlack, displayName: "Black, TableStyleMedium15" },
    TableStyleMedium16: buildPreset("TableStyleMedium16", mediumMinimalBorders, COLOR_SETS.lightBlue),
    TableStyleMedium17: buildPreset("TableStyleMedium17", mediumMinimalBorders, COLOR_SETS.red),
    TableStyleMedium18: buildPreset("TableStyleMedium18", mediumMinimalBorders, COLOR_SETS.lightGreen),
    TableStyleMedium19: buildPreset("TableStyleMedium19", mediumMinimalBorders, COLOR_SETS.purple),
    TableStyleMedium20: buildPreset("TableStyleMedium20", mediumMinimalBorders, COLOR_SETS.gray),
    TableStyleMedium21: buildPreset("TableStyleMedium21", mediumMinimalBorders, COLOR_SETS.orange),
    TableStyleMedium22: buildPreset("TableStyleMedium22", mediumAllBorders, COLOR_SETS.black),
    TableStyleMedium23: buildPreset("TableStyleMedium23", mediumAllBorders, COLOR_SETS.lightBlue),
    TableStyleMedium24: buildPreset("TableStyleMedium24", mediumAllBorders, COLOR_SETS.red),
    TableStyleMedium25: buildPreset("TableStyleMedium25", mediumAllBorders, COLOR_SETS.lightGreen),
    TableStyleMedium26: buildPreset("TableStyleMedium26", mediumAllBorders, COLOR_SETS.purple),
    TableStyleMedium27: buildPreset("TableStyleMedium27", mediumAllBorders, COLOR_SETS.gray),
    TableStyleMedium28: buildPreset("TableStyleMedium28", mediumAllBorders, COLOR_SETS.orange),
    TableStyleDark1: { ...darkTemplateInBlack, displayName: "Black, TableStyleDark1" },
    TableStyleDark2: buildPreset("TableStyleDark2", dark, COLOR_SETS.lightBlue),
    TableStyleDark3: buildPreset("TableStyleDark3", dark, COLOR_SETS.red),
    TableStyleDark4: buildPreset("TableStyleDark4", dark, COLOR_SETS.lightGreen),
    TableStyleDark5: buildPreset("TableStyleDark5", dark, COLOR_SETS.purple),
    TableStyleDark6: buildPreset("TableStyleDark6", dark, COLOR_SETS.gray),
    TableStyleDark7: buildPreset("TableStyleDark7", dark, COLOR_SETS.orange),
    TableStyleDark8: buildPreset("TableStyleDark8", darkNoBorders, DARK_COLOR_SETS.black),
    TableStyleDark9: buildPreset("TableStyleDark9", darkNoBorders, DARK_COLOR_SETS.redBlue),
    TableStyleDark10: buildPreset("TableStyleDark10", darkNoBorders, DARK_COLOR_SETS.purpleGreen),
    TableStyleDark11: buildPreset("TableStyleDark11", darkNoBorders, DARK_COLOR_SETS.orangeBlue),
};
const TABLE_STYLES_TEMPLATES = {
    none: () => ({ category: "none", templateName: "none", primaryColor: "", name: "none" }),
    lightColoredText: lightColoredText,
    lightAllBorders: lightAllBorders,
    mediumAllBorders: mediumAllBorders,
    lightWithHeader: lightWithHeader,
    mediumBandedBorders: mediumBandedBorders,
    mediumMinimalBorders: mediumMinimalBorders,
    darkNoBorders: darkNoBorders,
    mediumWhiteBorders: mediumWhiteBorders,
    dark: dark,
};
function buildTableStyle(name, templateName, primaryColor) {
    const colorSet = generateTableColorSet("", primaryColor);
    return {
        ...TABLE_STYLES_TEMPLATES[templateName](colorSet),
        category: "custom",
        displayName: name,
    };
}

css /* scss */ `
  .o-spreadsheet {
    .o-icon {
      display: flex;
      align-items: center;
      justify-content: center;
      width: ${ICON_EDGE_LENGTH}px;
      height: ${ICON_EDGE_LENGTH}px;
      font-size: ${ICON_EDGE_LENGTH}px;
      vertical-align: middle;

      .small-text {
        font: bold 9px sans-serif;
      }
      .heavy-text {
        font: bold 16px sans-serif;
      }
    }
    .fa-small {
      font-size: 14px;
    }
  }
`;
// -----------------------------------------------------------------------------
// We need here the svg of the icons that we need to convert to images for the renderer
// -----------------------------------------------------------------------------
const ARROW_DOWN = {
    name: "ARROW_DOWN",
    width: 448,
    height: 512,
    paths: [
        {
            fillColor: "#E06666",
            path: "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",
        },
    ],
};
const ARROW_UP = {
    name: "ARROW_UP",
    width: 448,
    height: 512,
    paths: [
        {
            fillColor: "#6AA84F",
            path: "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",
        },
    ],
};
const ARROW_RIGHT = {
    name: "ARROW_RIGHT",
    width: 448,
    height: 512,
    paths: [
        {
            fillColor: "#F0AD4E",
            path: "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",
        },
    ],
};
const SMILE = {
    name: "SMILE",
    width: 496,
    height: 512,
    paths: [
        {
            fillColor: "#6AA84F",
            path: "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",
        },
    ],
};
const MEH = {
    name: "MEH",
    width: 496,
    height: 512,
    paths: [
        {
            fillColor: "#F0AD4E",
            path: "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",
        },
    ],
};
const FROWN = {
    name: "FROWN",
    width: 496,
    height: 512,
    paths: [
        {
            fillColor: "#E06666",
            path: "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",
        },
    ],
};
const DOT_PATH = "M256 9 a247 247 0 1 0.1 0 0";
const GREEN_DOT = {
    name: "GREEN_DOT",
    width: 512,
    height: 512,
    paths: [{ fillColor: "#6AA84F", path: DOT_PATH }],
};
const YELLOW_DOT = {
    name: "YELLOW_DOT",
    width: 512,
    height: 512,
    paths: [{ fillColor: "#F0AD4E", path: DOT_PATH }],
};
const RED_DOT = {
    name: "RED_DOT",
    width: 512,
    height: 512,
    paths: [{ fillColor: "#E06666", path: DOT_PATH }],
};
function getCaretDownSvg(color) {
    return {
        name: "CARET_DOWN",
        width: 512,
        height: 512,
        paths: [{ fillColor: color.textColor || TEXT_BODY_MUTED, path: "M120 195 h270 l-135 130" }],
    };
}
function getCaretUpSvg(color) {
    return {
        name: "CARET_UP",
        width: 512,
        height: 512,
        paths: [{ fillColor: color.textColor || TEXT_BODY_MUTED, path: "M120 325 h270 l-135 -130" }],
    };
}
function getHoveredCaretDownSvg(color) {
    return {
        name: "CARET_DOWN",
        width: 512,
        height: 512,
        paths: [
            { fillColor: color.textColor || TEXT_BODY_MUTED, path: "M15 15 h482 v482 h-482" },
            { fillColor: color.fillColor || "#fff", path: "M120 195 h270 l-135 130" },
        ],
    };
}
const CHIP_CARET_DOWN_PATH = "M40 185 h270 l-135 128";
function getChipSvg(chipStyle) {
    return {
        name: "CHIP",
        width: 512,
        height: 512,
        paths: [{ fillColor: chipStyle.textColor || TEXT_BODY_MUTED, path: CHIP_CARET_DOWN_PATH }],
    };
}
function getHoveredChipSvg(chipStyle) {
    return {
        name: "CHIP",
        width: 512,
        height: 512,
        paths: [
            {
                fillColor: chipStyle.textColor || TEXT_BODY_MUTED,
                path: "M0,225 A175,175 0 1,0 350,225 A175,175 0 1,0 0,225",
            },
            { fillColor: chipStyle.fillColor || TEXT_BODY_MUTED, path: CHIP_CARET_DOWN_PATH },
        ],
    };
}
const CHECKBOX_UNCHECKED = {
    name: "CHECKBOX_UNCHECKED",
    width: 512,
    height: 512,
    paths: [{ fillColor: GRAY_300, path: "M45,45 h422 v422 h-422 v-422 m30,30 v362 h362 v-362" }],
};
const CHECKBOX_UNCHECKED_HOVERED = {
    name: "CHECKBOX_UNCHECKED",
    width: 512,
    height: 512,
    paths: [{ fillColor: ACTION_COLOR, path: "M45,45 h422 v422 h-422 v-422 m30,30 v362 h362 v-362" }],
};
const CHECKBOX_CHECKED = {
    name: "CHECKBOX_CHECKED",
    width: 512,
    height: 512,
    paths: [
        { fillColor: ACTION_COLOR, path: "M45,45 h422 v422 h-422 v-422" },
        { fillColor: "#FFF", path: "M165,240 l45,45 l135,-135 h60 l-195,195 l-105,-105" },
    ],
};
function getPivotIconSvg(isCollapsed, isHovered) {
    const symbolPath = isCollapsed
        ? "M149,235 h213 v43 h-213 M235,149 h43 v213 h-43" // +
        : "M149,235 h213 v43 h-213"; // -
    return {
        name: "PIVOT_ICON",
        width: 512,
        height: 512,
        paths: [
            { path: "M21,21 h469 v469 h-469", fillColor: isHovered ? GRAY_900 : "#777" }, // borders
            { path: "M64,64 v384 h384 v-384", fillColor: isHovered ? GRAY_200 : "#eee" }, // background
            { path: symbolPath, fillColor: isHovered ? GRAY_900 : "#777" },
        ],
    };
}
function getDataFilterIcon(isActive, isHighContrast, isHovered) {
    const symbolPath = isActive
        ? "M18.6 3.5H4.29c-.7 0-1.06.85-.56 1.35l6.1 6.1v6.8c0 .26.13.5.34.65l2.64 1.85a.79.79 0 0 0 1.25-.65v-8.64l6.1-6.1a.79.79 0 0 0-.56-1.35"
        : "M 339.667 681 L 510.333 681 L 510.333 595.667 L 339.667 595.667 L 339.667 681 Z M 41 169 L 41 254.333 L 809 254.333 L 809 169 L 41 169 Z M 169 467.667 L 681 467.667 L 681 382.333 L 169 382.333 L 169 467.667 Z";
    const hoverBackgroundPath = isActive ? "M0,0 h24 v24 h-24" : "M0,0 h850 v850 h-850";
    const colors = { iconColor: FILTERS_COLOR, hoverBackgroundColor: FILTERS_COLOR };
    if (isHovered && !isHighContrast) {
        colors.iconColor = "#fff";
    }
    else if (!isHovered && isHighContrast) {
        colors.iconColor = "#defade";
    }
    else if (isHovered && isHighContrast) {
        colors.iconColor = FILTERS_COLOR;
        colors.hoverBackgroundColor = "#fff";
    }
    return {
        name: "DATA_FILTER_ICON",
        width: isActive ? 24 : 850,
        height: isActive ? 24 : 850,
        paths: [
            isHovered ? { path: hoverBackgroundPath, fillColor: colors.hoverBackgroundColor } : undefined,
            { path: symbolPath, fillColor: colors.iconColor },
        ].filter(isDefined),
    };
}
const ICONS = {
    arrowGood: {
        template: "ARROW_UP",
        svg: ARROW_UP,
    },
    arrowNeutral: {
        template: "ARROW_RIGHT",
        svg: ARROW_RIGHT,
    },
    arrowBad: {
        template: "ARROW_DOWN",
        svg: ARROW_DOWN,
    },
    smileyGood: {
        template: "SMILE",
        svg: SMILE,
    },
    smileyNeutral: {
        template: "MEH",
        svg: MEH,
    },
    smileyBad: {
        template: "FROWN",
        svg: FROWN,
    },
    dotGood: {
        template: "GREEN_DOT",
        svg: GREEN_DOT,
    },
    dotNeutral: {
        template: "YELLOW_DOT",
        svg: YELLOW_DOT,
    },
    dotBad: {
        template: "RED_DOT",
        svg: 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",
    },
};
const path2DCache = {};
function getPath2D(svgPath) {
    if (path2DCache[svgPath]) {
        return path2DCache[svgPath];
    }
    const path2D = new Path2D(svgPath);
    path2DCache[svgPath] = path2D;
    return path2D;
}

/**
 * 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;
}
/**
 * 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));
    });
}
/** Get a header in the SheetData. Create the header if it doesn't exist in the SheetData */
function getSheetDataHeader(sheetData, dimension, index) {
    if (dimension === "COL") {
        if (!sheetData.cols[index]) {
            sheetData.cols[index] = {};
        }
        return sheetData.cols[index];
    }
    if (!sheetData.rows[index]) {
        sheetData.rows[index] = {};
    }
    return sheetData.rows[index];
}
/** Prefix the string by "=" if the string looks like a formula */
function prefixFormulaWithEqual(formula) {
    if (formula[0] === "=") {
        return formula;
    }
    const tokens = tokenize(formula);
    return tokens.length === 1 && tokens[0].type !== "REFERENCE" ? formula : "=" + formula;
}

/**
 * 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["TimeDataValidationNotSupported"] = "Time data validation rules";
    WarningTypes["TextLengthDataValidationNotSupported"] = "Text length data validation rules";
    WarningTypes["WholeNumberDataValidationNotSupported"] = "Whole number data validation rules";
    WarningTypes["NotEqualDateDataValidationNotSupported"] = "Not equal date data validation rules";
})(WarningTypes || (WarningTypes = {}));
class XLSXImportWarningManager {
    _parsingWarnings = new Set();
    _conversionWarnings = new Set();
    addParsingWarning(warning) {
        this._parsingWarnings.add(warning);
    }
    addConversionWarning(warning) {
        this._conversionWarnings.add(warning);
    }
    get warnings() {
        return [...this._parsingWarnings, ...this._conversionWarnings];
    }
    /**
     * Add a warning "... is not supported" to the manager.
     *
     * @param type the type of the warning to add
     * @param name optional, name of the element that was not supported
     * @param supported optional, list of the supported elements
     */
    generateNotSupportedWarning(type, name, supported) {
        let warning = `${type} ${name ? '"' + name + '" is' : "are"} not yet supported. `;
        if (supported) {
            warning += `Only ${supported.join(", ")} are currently supported.`;
        }
        if (!this._conversionWarnings.has(warning)) {
            this._conversionWarnings.add(warning);
        }
    }
}

const SUPPORTED_BORDER_STYLES = ["thin", "medium", "thick", "dashed", "dotted"];
const SUPPORTED_HORIZONTAL_ALIGNMENTS = [
    "general",
    "left",
    "center",
    "right",
];
const SUPPORTED_VERTICAL_ALIGNMENTS = ["top", "center", "bottom"];
const SUPPORTED_FONTS = ["Arial"];
const SUPPORTED_FILL_PATTERNS = ["solid", "none"];
const SUPPORTED_CF_TYPES = [
    "expression",
    "cellIs",
    "colorScale",
    "iconSet",
    "containsText",
    "notContainsText",
    "beginsWith",
    "endsWithText",
    "containsBlanks",
    "notContainsBlanks",
];
/** Map between cell type in XLSX file and human readable cell type  */
const CELL_TYPE_CONVERSION_MAP = {
    b: "boolean",
    d: "date",
    e: "error",
    inlineStr: "inlineStr",
    n: "number",
    s: "sharedString",
    str: "str",
};
/** Conversion map Border Style in XLSX <=> Border style in o_spreadsheet*/
const BORDER_STYLE_CONVERSION_MAP = {
    dashDot: "thin",
    dashDotDot: "thin",
    dashed: "dashed",
    dotted: "dotted",
    double: "thin",
    hair: "thin",
    medium: "medium",
    mediumDashDot: "thin",
    mediumDashDotDot: "thin",
    mediumDashed: "thin",
    none: undefined,
    slantDashDot: "thin",
    thick: "thick",
    thin: "thin",
};
/** Conversion map Horizontal Alignment in XLSX <=> Horizontal Alignment in o_spreadsheet*/
const H_ALIGNMENT_CONVERSION_MAP = {
    general: undefined,
    left: "left",
    center: "center",
    right: "right",
    fill: "left",
    justify: "left",
    centerContinuous: "center",
    distributed: "center",
};
/** Conversion map Vertical Alignment in XLSX => Vertical Alignment in o_spreadsheet */
const V_ALIGNMENT_CONVERSION_MAP = {
    top: "top",
    center: "middle",
    bottom: "bottom",
    justify: "middle",
    distributed: "middle",
};
/** Conversion map Vertical Alignment in o-spreadsheet => Vertical Alignment in XLSX */
const V_ALIGNMENT_EXPORT_CONVERSION_MAP = {
    top: "top",
    middle: "center",
    bottom: "bottom",
};
/** Conversion map CF types in XLSX <=> Cf types in o_spreadsheet */
const CF_OPERATOR_TYPE_CONVERSION_MAP = {
    beginsWith: "beginsWithText",
    endsWith: "endsWithText",
    containsText: "containsText",
    notContains: "notContainsText",
    between: "isBetween",
    notBetween: "isNotBetween",
    equal: "isEqual",
    notEqual: "isNotEqual",
    greaterThan: "isGreaterThan",
    greaterThanOrEqual: "isGreaterOrEqualTo",
    lessThan: "isLessThan",
    lessThanOrEqual: "isLessOrEqualTo",
};
/** Conversion map CF types in XLSX <=> Cf types in o_spreadsheet */
const CF_TYPE_CONVERSION_MAP = {
    aboveAverage: undefined,
    expression: "customFormula",
    cellIs: undefined, // exist but isn't an operator in o_spreadsheet
    colorScale: undefined, // exist but isn't an operator in o_spreadsheet
    dataBar: undefined,
    iconSet: undefined, // exist but isn't an operator in o_spreadsheet
    top10: undefined,
    uniqueValues: undefined,
    duplicateValues: undefined,
    containsText: "containsText",
    notContainsText: "notContainsText",
    beginsWith: "beginsWithText",
    endsWith: "endsWithText",
    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: "radar",
    scatterChart: "scatter",
    pieChart: "pie",
    pie3DChart: undefined,
    doughnutChart: "pie",
    barChart: "bar",
    bar3DChart: undefined,
    ofPieChart: undefined,
    surfaceChart: undefined,
    surface3DChart: undefined,
    bubbleChart: undefined,
    comboChart: "combo",
};
const EXCEL_TO_SPREADSHEET_TRENDLINE_TYPE_MAPPING = {
    exp: "exponential",
    log: "logarithmic",
    poly: "polynomial",
    movingAvg: "trailingMovingAverage",
};
/** 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: "General",
    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: "@",
};
/**
 * 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", // system foreground
    65: "FFFFFF", // system background
};
const IMAGE_MIMETYPE_TO_EXTENSION_MAPPING = {
    "image/avif": "avif",
    "image/bmp": "bmp",
    "image/gif": "gif",
    "image/vnd.microsoft.icon": "ico",
    "image/jpeg": "jpeg",
    "image/png": "png",
    "image/tiff": "tiff",
    "image/webp": "webp",
};
const IMAGE_EXTENSION_TO_MIMETYPE_MAPPING = {
    avif: "image/avif",
    bmp: "image/bmp",
    gif: "image/gif",
    ico: "image/vnd.microsoft.icon",
    jpeg: "image/jpeg",
    png: "image/png",
    tiff: "image/tiff",
    webp: "image/webp",
    jpg: "image/jpeg",
};
const XLSX_DV_DECIMAL_OPERATOR_MAPPING = {
    between: "isBetween",
    notBetween: "isNotBetween",
    equal: "isEqual",
    notEqual: "isNotEqual",
    greaterThan: "isGreaterThan",
    greaterThanOrEqual: "isGreaterOrEqualTo",
    lessThan: "isLessThan",
    lessThanOrEqual: "isLessOrEqualTo",
};
const XLSX_DV_DATE_OPERATOR_TO_DV_TYPE_MAPPING = {
    between: "dateIsBetween",
    notBetween: "dateIsNotBetween",
    equal: "dateIs",
    greaterThan: "dateIsAfter",
    greaterThanOrEqual: "dateIsOnOrAfter",
    lessThan: "dateIsBefore",
    lessThanOrEqual: "dateIsOnOrBefore",
};

/**
 * 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);
}
/**
 * When defining style (fontColor, borderColor for instance)
 * Excel will specify rgb="FF000000"
 * In that case, We should not consider this value as user-defined but
 * rather like an instruction: "Use your system default"
 */
const DEFAULT_SYSTEM_COLOR = "FF000000";

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) {
    // Format is either defined in the imported data, or the formatId is defined in openXML §18.8.30
    const format = XLSX_FORMATS_CONVERSION_MAP[numFmtId] || formats.find((f) => f.id === numFmtId)?.format;
    if (format === "General") {
        return undefined;
    }
    if (format) {
        try {
            let convertedFormat = format.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
            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
            if (isXlsxDateFormat(convertedFormat)) {
                convertedFormat = convertDateFormat$1(convertedFormat);
            }
            if (isFormatSupported(convertedFormat)) {
                return convertedFormat;
            }
        }
        catch (e) { }
    }
    warningManager.generateNotSupportedWarning(WarningTypes.NumFmtIdNotSupported, format || `nmFmtId ${numFmtId}`);
    return undefined;
}
function isFormatSupported(format) {
    try {
        formatValue(0, { format, locale: DEFAULT_LOCALE });
        return true;
    }
    catch (e) {
        return false;
    }
}
function isXlsxDateFormat(format) {
    return XLSX_DATE_FORMAT_REGEX.test(format);
}
function convertDateFormat$1(format) {
    // Some of these aren't defined neither in the OpenXML spec not the Xlsx extension of OpenXML,
    // but can still occur and are supported by Excel/Google sheets
    format = format.toLowerCase();
    format = format.replace(/mmmmm/g, "mmm");
    format = format.replace(/am\/pm|a\/m/g, "a");
    format = format.replace(/hhhh/g, "hh");
    format = format.replace(/\bh\b/g, "hh");
    return format;
}

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

function convertConditionalFormats(xlsxCfs, dxfs, warningManager) {
    const cfs = [];
    let cfId = 1;
    for (const 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" || rule.type === "dataBar"))
            continue;
        switch (rule.type) {
            case "aboveAverage":
            case "containsErrors":
            case "notContainsErrors":
            case "duplicateValues":
            case "top10":
            case "uniqueValues":
            case "timePeriod":
                // Not supported
                continue;
            case "dataBar":
                const dataBar = convertDataBar(cfId++, cf);
                if (dataBar) {
                    cfs.push(dataBar);
                }
                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 "expression":
                if (!rule.formula?.length)
                    continue;
                operator = CF_TYPE_CONVERSION_MAP[rule.type];
                values.push(`=${rule.formula[0]}`);
                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 = CF_OPERATOR_TYPE_CONVERSION_MAP[rule.operator];
                values.push(prefixFormulaWithEqual(rule.formula[0]));
                if (rule.formula.length === 2) {
                    values.push(prefixFormulaWithEqual(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 convertDataBar(id, xlsxCf) {
    const dataBar = xlsxCf.cfRules[0].dataBar;
    if (!dataBar)
        return undefined;
    const color = hexaToInt(convertColor(dataBar.color) || "#FFFFFF");
    return {
        id: id.toString(),
        stopIfTrue: xlsxCf.cfRules[0].stopIfTrue,
        ranges: xlsxCf.sqref,
        rule: {
            type: "DataBarRule",
            color,
        },
    };
}
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 (const 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 "notContainsText":
            return "notContainsBlanks";
        case "containsText":
            return "containsText";
        case "beginsWithText":
            return "beginsWith";
        case "endsWithText":
            return "endsWith";
        case "isGreaterThan":
            return "greaterThan";
        case "isGreaterOrEqualTo":
            return "greaterThanOrEqual";
        case "isLessThan":
            return "lessThan";
        case "isLessOrEqualTo":
            return "lessThanOrEqual";
        case "isBetween":
            return "between";
        case "isNotBetween":
            return "notBetween";
        case "isEqual":
            return "equal";
        case "isNotEqual":
            return "notEqual";
        case "customFormula":
            return "";
    }
}
// -------------------------------------
//        WORKSHEET HELPERS
// -------------------------------------
function getCellType(value) {
    switch (typeof value) {
        case "boolean":
            return "b";
        case "string":
            return "str";
        case "number":
            return "n";
        default:
            return undefined;
    }
}
function convertHeightToExcel(height) {
    return Math.round(HEIGHT_FACTOR * height * 100) / 100;
}
function convertWidthToExcel(width) {
    return Math.round(WIDTH_FACTOR * width * 100) / 100;
}
function convertHeightFromExcel(height) {
    if (!height)
        return height;
    return Math.round((height / HEIGHT_FACTOR) * 100) / 100;
}
function convertWidthFromExcel(width) {
    if (!width)
        return width;
    return Math.round((width / WIDTH_FACTOR) * 100) / 100;
}
function extractStyle(data, content, styleId, formatId, borderId) {
    const style = styleId ? data.styles[styleId] : {};
    const format = formatId ? data.formats[formatId] : undefined;
    const styles = {
        font: {
            size: style?.fontSize || DEFAULT_FONT_SIZE,
            color: { rgb: style?.textColor ? style.textColor : "000000" },
            family: 2,
            name: "Arial",
        },
        fill: style?.fillColor
            ? {
                fgColor: { rgb: style.fillColor },
            }
            : { reservedAttribute: "none" },
        numFmt: format ? { format: format, id: 0 /* id not used for export */ } : undefined,
        border: borderId || 0,
        alignment: {
            horizontal: style.align,
            vertical: style.verticalAlign
                ? V_ALIGNMENT_EXPORT_CONVERSION_MAP[style.verticalAlign]
                : undefined,
            wrapText: style.wrapping === "wrap" || content?.includes(NEWLINE) ? true : undefined,
            shrinkToFit: style.wrapping === "clip" ? true : undefined,
        },
    };
    styles.font["strike"] = !!style?.strikethrough || undefined;
    styles.font["underline"] = !!style?.underline || undefined;
    styles.font["bold"] = !!style?.bold || undefined;
    styles.font["italic"] = !!style?.italic || undefined;
    return styles;
}
function 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,
            wrapText: styles.alignment.wrapText,
            shrinkToFit: styles.alignment.shrinkToFit,
        },
    };
    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) {
    const 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;
}
const globalReverseLookup = new WeakMap();
function pushElement(property, propertyList) {
    let reverseLookup = globalReverseLookup.get(propertyList);
    if (!reverseLookup) {
        reverseLookup = new Map();
        for (let i = 0; i < propertyList.length; i++) {
            const canonical = getCanonicalRepresentation(propertyList[i]);
            reverseLookup.set(canonical, i);
        }
        globalReverseLookup.set(propertyList, reverseLookup);
    }
    const canonical = getCanonicalRepresentation(property);
    if (reverseLookup.has(canonical)) {
        return reverseLookup.get(canonical);
    }
    const maxId = propertyList.length;
    propertyList.push(property);
    reverseLookup.set(canonical, maxId);
    return maxId;
}
/**
 * Convert a chart o-spreadsheet id to a xlsx id which
 * are unsigned integers (starting from 1).
 */
function convertChartId(chartId, construct) {
    const xlsxId = construct.chartIds.findIndex((id) => id === chartId);
    if (xlsxId === -1) {
        construct.chartIds.push(chartId);
        return construct.chartIds.length;
    }
    return xlsxId + 1;
}
const imageIds = [];
/**
 * Convert a image o-spreadsheet id to a xlsx id which
 * are unsigned integers (starting from 1).
 */
function convertImageId(imageId) {
    const xlsxId = imageIds.findIndex((id) => id === imageId);
    if (xlsxId === -1) {
        imageIds.push(imageId);
        return imageIds.length;
    }
    return xlsxId + 1;
}
/**
 * Convert a value expressed in dot to EMU.
 * EMU = English Metrical Unit
 * There are 914400 EMU per inch.
 *
 * /!\ A value expressed in EMU cannot be fractional.
 * See https://docs.microsoft.com/en-us/windows/win32/vml/msdn-online-vml-units#other-units-of-measurement
 */
function convertDotValueToEMU(value) {
    const DPI = 96;
    return Math.round((value * 914400) / DPI);
}
function getRangeSize(reference, defaultSheetIndex, data) {
    let xc = reference;
    let sheetName = undefined;
    ({ xc, sheetName } = splitReference(reference));
    let rangeSheetIndex;
    if (sheetName) {
        const index = data.sheets.findIndex((sheet) => 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) {
    let position = 0;
    for (let i = 0; i < colIndex; i++) {
        const colAtIndex = sheetData.cols.find((col) => i >= col.min && i <= col.max);
        if (colAtIndex?.width) {
            position += colAtIndex.width;
        }
        else if (sheetData.sheetFormat?.defaultColWidth) {
            position += sheetData.sheetFormat.defaultColWidth;
        }
        else {
            position += EXCEL_DEFAULT_COL_WIDTH;
        }
    }
    return position / WIDTH_FACTOR;
}
/**
 * Get the position of the start of a row in Excel (in px).
 */
function getRowPosition(rowIndex, sheetData) {
    let position = 0;
    for (let i = 0; i < rowIndex; i++) {
        const rowAtIndex = sheetData.rows.find((row) => row.index - 1 === i);
        if (rowAtIndex?.height) {
            position += rowAtIndex.height;
        }
        else if (sheetData.sheetFormat?.defaultRowHeight) {
            position += sheetData.sheetFormat.defaultRowHeight;
        }
        else {
            position += EXCEL_DEFAULT_ROW_HEIGHT;
        }
    }
    return position / HEIGHT_FACTOR;
}
/**
 * Convert the o-spreadsheet data validation decimal
 * criterion type to the corresponding excel operator.
 */
function convertDecimalCriterionTypeToExcelOperator(operator) {
    return Object.keys(XLSX_DV_DECIMAL_OPERATOR_MAPPING).find((key) => XLSX_DV_DECIMAL_OPERATOR_MAPPING[key] === operator);
}
/**
 * Convert the o-spreadsheet data validation date
 * criterion type to the corresponding excel operator.
 */
function convertDateCriterionTypeToExcelOperator(operator) {
    return Object.keys(XLSX_DV_DATE_OPERATOR_TO_DV_TYPE_MAPPING).find((key) => XLSX_DV_DATE_OPERATOR_TO_DV_TYPE_MAPPING[key] === operator);
}

function convertFigures(sheetData) {
    let id = 1;
    return sheetData.figures
        .map((figure) => convertFigure(figure, (id++).toString(), sheetData))
        .filter(isDefined);
}
function convertFigure(figure, id, sheetData) {
    let col;
    let row;
    let offset;
    let height, width;
    if (figure.anchors.length === 1) {
        // one cell anchor
        ({ col, row, offset } = convertAnchor(figure.anchors[0]));
        width = convertEMUToDotValue(figure.figureSize.cx);
        height = convertEMUToDotValue(figure.figureSize.cy);
    }
    else {
        ({ col, row, offset } = convertAnchor(figure.anchors[0]));
        const { x: x1, y: y1 } = getPositionFromAnchor(figure.anchors[0], sheetData);
        const { x: x2, y: y2 } = getPositionFromAnchor(figure.anchors[1], sheetData);
        width = x2 - x1;
        height = y2 - y1;
    }
    const figureData = { id, col, row, offset };
    if (isChartData(figure.data)) {
        return {
            ...figureData,
            width,
            height,
            tag: "chart",
            data: convertChartData(figure.data),
        };
    }
    else if (isImageData(figure.data)) {
        return {
            ...figureData,
            width: convertEMUToDotValue(figure.data.size.cx),
            height: convertEMUToDotValue(figure.data.size.cy),
            tag: "image",
            data: {
                path: figure.data.imageSrc,
                mimetype: figure.data.mimetype,
            },
        };
    }
    return undefined;
}
function isChartData(data) {
    return "dataSets" in data && data.dataSets.length > 0;
}
function isImageData(data) {
    return "imageSrc" in data;
}
function convertChartData(chartData) {
    const dataSetsHaveTitle = chartData.dataSets.some((ds) => "reference" in (ds.label ?? {}));
    const labelRange = chartData.labelRange
        ? convertExcelRangeToSheetXC(chartData.labelRange, dataSetsHaveTitle)
        : undefined;
    const dataSets = chartData.dataSets.map((data) => {
        let label = undefined;
        if (data.label && "text" in data.label) {
            label = data.label.text;
        }
        return {
            dataRange: convertExcelRangeToSheetXC(data.range, dataSetsHaveTitle),
            label,
            backgroundColor: data.backgroundColor,
            trend: convertExcelTrendline(data.trend),
        };
    });
    // For doughnut charts, in chartJS first dataset = outer dataset, in excel first dataset = inner dataset
    if (chartData.type === "pie") {
        dataSets.reverse();
    }
    const creationContext = {
        range: dataSets,
        dataSetsHaveTitle,
        auxiliaryRange: labelRange,
        title: chartData.title ?? { text: "" },
        background: convertColor({ rgb: chartData.backgroundColor }) || "#FFFFFF",
        legendPosition: chartData.legendPosition,
        stacked: chartData.stacked || false,
        aggregated: false,
        cumulative: chartData.cumulative || false,
        labelsAsText: false,
        horizontal: chartData.horizontal,
        isDoughnut: chartData.isDoughnut,
        pieHolePercentage: chartData.pieHolePercentage,
    };
    try {
        const ChartClass = chartRegistry.get(chartData.type);
        return ChartClass.getChartDefinitionFromContextCreation(creationContext);
    }
    catch (e) {
        return undefined;
    }
}
function convertExcelRangeToSheetXC(range, dataSetsHaveTitle) {
    const { sheetName, xc } = splitReference(range);
    let zone = toUnboundedZone(xc);
    if (dataSetsHaveTitle && zone.bottom !== undefined && zone.right !== undefined) {
        const height = zone.bottom - zone.top + 1;
        const width = zone.right - zone.left + 1;
        if (height === 1) {
            zone = { ...zone, left: zone.left - 1 };
        }
        else if (width === 1) {
            zone = { ...zone, top: zone.top - 1 };
        }
    }
    const dataXC = zoneToXc(zone);
    return getFullReference(sheetName, dataXC);
}
function convertExcelTrendline(trend) {
    if (!trend || !trend.type) {
        return undefined;
    }
    return {
        type: trend.type === "linear"
            ? "polynomial"
            : EXCEL_TO_SPREADSHEET_TRENDLINE_TYPE_MAPPING[trend.type],
        order: trend.type === "linear" ? 1 : trend.order,
        color: trend.color,
        window: trend.window || DEFAULT_WINDOW_SIZE,
        display: true,
    };
}
function convertAnchor(XLSXanchor) {
    const offset = {
        x: convertEMUToDotValue(XLSXanchor.colOffset) - FIGURE_BORDER_WIDTH,
        y: convertEMUToDotValue(XLSXanchor.rowOffset) - FIGURE_BORDER_WIDTH,
    };
    return { col: XLSXanchor.col, row: XLSXanchor.row, offset };
}
function getPositionFromAnchor(anchor, sheetData) {
    return {
        x: getColPosition(anchor.col, sheetData) + convertEMUToDotValue(anchor.colOffset),
        y: getRowPosition(anchor.row, sheetData) + convertEMUToDotValue(anchor.rowOffset),
    };
}

function convertDataValidationRules(xlsxDataValidations, warningManager) {
    const dvRules = [];
    let dvId = 1;
    for (const dv of xlsxDataValidations) {
        if (!dv) {
            continue;
        }
        switch (dv.type) {
            case "time":
                warningManager.generateNotSupportedWarning(WarningTypes.TimeDataValidationNotSupported);
                break;
            case "textLength":
                warningManager.generateNotSupportedWarning(WarningTypes.TextLengthDataValidationNotSupported);
                break;
            case "whole":
                warningManager.generateNotSupportedWarning(WarningTypes.WholeNumberDataValidationNotSupported);
                break;
            case "decimal":
                const decimalRule = convertDecimalRule(dvId++, dv);
                dvRules.push(decimalRule);
                break;
            case "list":
                const listRule = convertListRule(dvId++, dv);
                dvRules.push(listRule);
                break;
            case "date":
                if (dv.operator === "notEqual") {
                    warningManager.generateNotSupportedWarning(WarningTypes.NotEqualDateDataValidationNotSupported);
                    break;
                }
                const dateRule = convertDateRule(dvId++, dv);
                dvRules.push(dateRule);
                break;
            case "custom":
                const customRule = convertCustomRule(dvId++, dv);
                dvRules.push(customRule);
                break;
        }
    }
    return dvRules;
}
function convertDecimalRule(id, dv) {
    const values = [prefixFormulaWithEqual(dv.formula1.toString())];
    if (dv.formula2) {
        values.push(prefixFormulaWithEqual(dv.formula2.toString()));
    }
    return {
        id: id.toString(),
        ranges: dv.sqref,
        isBlocking: dv.errorStyle !== "warning",
        criterion: {
            type: XLSX_DV_DECIMAL_OPERATOR_MAPPING[dv.operator],
            values,
        },
    };
}
function convertListRule(id, dv) {
    const formula1 = dv.formula1.toString();
    const isRangeRule = rangeReference.test(formula1);
    return {
        id: id.toString(),
        ranges: dv.sqref,
        isBlocking: dv.errorStyle !== "warning",
        criterion: {
            type: isRangeRule ? "isValueInRange" : "isValueInList",
            values: isRangeRule ? [formula1] : formula1.replaceAll('"', "").split(","),
            displayStyle: "arrow",
        },
    };
}
function convertDateRule(id, dv) {
    let criterion;
    const values = [prefixFormulaWithEqual(dv.formula1.toString())];
    if (dv.formula2) {
        values.push(prefixFormulaWithEqual(dv.formula2.toString()));
        criterion = {
            type: XLSX_DV_DATE_OPERATOR_TO_DV_TYPE_MAPPING[dv.operator],
            values: getDateCriterionFormattedValues(values, DEFAULT_LOCALE),
        };
    }
    else {
        criterion = {
            type: XLSX_DV_DATE_OPERATOR_TO_DV_TYPE_MAPPING[dv.operator],
            values: getDateCriterionFormattedValues(values, DEFAULT_LOCALE),
            dateValue: "exactDate",
        };
    }
    return {
        id: id.toString(),
        ranges: dv.sqref,
        isBlocking: dv.errorStyle !== "warning",
        criterion: criterion,
    };
}
function convertCustomRule(id, dv) {
    return {
        id: id.toString(),
        ranges: dv.sqref,
        isBlocking: dv.errorStyle !== "warning",
        criterion: {
            type: "customFormula",
            values: [prefixFormulaWithEqual(dv.formula1.toString())],
        },
    };
}

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

function convertSheets(data, warningManager) {
    return data.sheets.map((sheet) => {
        convertFormulasContent(sheet, data);
        const sheetDims = getSheetDims(sheet);
        const sheetOptions = sheet.sheetViews[0];
        const rowHeaderGroups = convertHeaderGroup(sheet, "ROW", sheetDims[1]);
        const colHeaderGroups = convertHeaderGroup(sheet, "COL", sheetDims[0]);
        return {
            id: sheet.sheetName,
            areGridLinesVisible: sheetOptions ? sheetOptions.showGridLines : true,
            name: sheet.sheetName,
            colNumber: sheetDims[0],
            rowNumber: sheetDims[1],
            ...convertCells(sheet, data, sheetDims, warningManager),
            merges: sheet.merges,
            cols: convertCols(sheet, sheetDims[0], colHeaderGroups),
            rows: convertRows(sheet, sheetDims[1], rowHeaderGroups),
            conditionalFormats: convertConditionalFormats(sheet.cfs, data.dxfs, warningManager),
            dataValidationRules: convertDataValidationRules(sheet.dataValidations, warningManager),
            figures: convertFigures(sheet),
            isVisible: sheet.isVisible,
            panes: sheetOptions
                ? { xSplit: sheetOptions.pane.xSplit, ySplit: sheetOptions.pane.ySplit }
                : { xSplit: 0, ySplit: 0 },
            tables: [],
            headerGroups: { COL: colHeaderGroups, ROW: rowHeaderGroups },
            color: convertColor(sheet.sheetProperties?.tabColor),
        };
    });
}
function convertCols(sheet, numberOfCols, headerGroups) {
    const cols = {};
    // Excel begins indexes at 1
    for (let i = 1; i < numberOfCols + 1; i++) {
        const col = sheet.cols.find((col) => col.min <= i && i <= col.max);
        let colSize;
        if (col && col.width)
            colSize = col.width;
        else if (sheet.sheetFormat?.defaultColWidth)
            colSize = sheet.sheetFormat.defaultColWidth;
        else
            colSize = EXCEL_DEFAULT_COL_WIDTH;
        // In xlsx there is no difference between hidden columns and columns inside a folded group.
        // But in o-spreadsheet folded columns are not considered hidden.
        const colIndex = i - 1;
        const isColFolded = headerGroups.some((group) => group.isFolded && group.start <= colIndex && colIndex <= group.end);
        cols[colIndex] = {
            size: convertWidthFromExcel(colSize),
            isHidden: !isColFolded && col?.hidden,
        };
    }
    return cols;
}
function convertRows(sheet, numberOfRows, headerGroups) {
    const rows = {};
    // Excel begins indexes at 1
    for (let i = 1; i < numberOfRows + 1; i++) {
        const row = sheet.rows.find((row) => row.index === i);
        let rowSize;
        if (row && row.height)
            rowSize = row.height;
        else if (sheet.sheetFormat?.defaultRowHeight)
            rowSize = sheet.sheetFormat.defaultRowHeight;
        else
            rowSize = EXCEL_DEFAULT_ROW_HEIGHT;
        // In xlsx there is no difference between hidden rows and rows inside a folded group.
        // But in o-spreadsheet folded rows are not considered hidden.
        const rowIndex = i - 1;
        const isRowFolded = headerGroups.some((group) => group.isFolded && group.start <= rowIndex && rowIndex <= group.end);
        rows[rowIndex] = {
            size: convertHeightFromExcel(rowSize),
            isHidden: !isRowFolded && row?.hidden,
        };
    }
    return rows;
}
function convertSharedStrings(xlsxSharedStrings) {
    return xlsxSharedStrings.map(replaceNewLines);
}
function convertCells(sheet, data, sheetDims, warningManager) {
    const cells = {};
    const styles = {};
    const formats = {};
    const borders = {};
    const sharedStrings = convertSharedStrings(data.sharedStrings);
    const hyperlinkMap = sheet.hyperlinks.reduce((map, link) => {
        map[link.xc] = link;
        return map;
    }, {});
    for (const row of sheet.rows) {
        for (const cell of row.cells) {
            cells[cell.xc] = getCellValue(cell, hyperlinkMap, sharedStrings, warningManager);
            if (cell.styleIndex) {
                // + 1 : our indexes for normalized values begin at 1 and not 0
                styles[cell.xc] = cell.styleIndex + 1;
                formats[cell.xc] = data.styles[cell.styleIndex].numFmtId + 1;
                borders[cell.xc] = data.styles[cell.styleIndex].borderId + 1;
            }
        }
    }
    // Apply row style
    for (const 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
            styles[xc] ??= row.styleIndex + 1;
            borders[xc] ??= data.styles[row.styleIndex].borderId + 1;
            formats[xc] ??= data.styles[row.styleIndex].numFmtId + 1;
        }
    }
    // Apply col style
    for (const 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
                styles[xc] ??= col.styleIndex + 1;
                borders[xc] ??= data.styles[col.styleIndex].borderId + 1;
                formats[xc] ??= data.styles[col.styleIndex].numFmtId + 1;
            }
        }
    }
    return { cells, styles, formats, borders };
}
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 (const 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);
    }
    for (const fig of sheet.figures) {
        dims[0] = Math.max(dims[0], fig.anchors[fig.anchors.length - 1]?.col ?? 0);
        dims[1] = Math.max(dims[1], fig.anchors[fig.anchors.length - 1]?.row ?? 0);
    }
    dims[0] = Math.max(dims[0] + 5, EXCEL_IMPORT_DEFAULT_NUMBER_OF_COLS);
    dims[1] = Math.max(dims[1] + 5, EXCEL_IMPORT_DEFAULT_NUMBER_OF_ROWS);
    return dims;
}
/**
 * Get the header groups from the XLS file.
 *
 * See ASCII art in HeaderGroupingPlugin.exportForExcel() for details on how the groups are defined in the xlsx.
 */
function convertHeaderGroup(sheet, dim, numberOfHeaders) {
    const outlineProperties = sheet?.sheetProperties?.outlinePr;
    const headerGroups = [];
    let currentLayer = 0;
    for (let i = 0; i < numberOfHeaders; i++) {
        const header = getHeader(sheet, dim, i);
        const headerLayer = header?.outlineLevel || 0;
        if (headerLayer > currentLayer) {
            // Whether the flag indicating if the group is collapsed is on the header before or after the group. Default is after.
            const collapseFlagAfter = (dim === "ROW" ? outlineProperties?.summaryBelow : outlineProperties?.summaryRight) ?? true;
            const group = computeHeaderGroup(sheet, dim, i, collapseFlagAfter);
            if (group) {
                headerGroups.push(group);
            }
        }
        currentLayer = headerLayer;
    }
    return headerGroups;
}
function computeHeaderGroup(sheet, dim, startIndex, collapseFlagAfter) {
    const startHeader = getHeader(sheet, dim, startIndex);
    const startLayer = startHeader?.outlineLevel;
    if (!startLayer || !startLayer) {
        return undefined;
    }
    let currentLayer = startLayer;
    let currentIndex = startIndex;
    let currentHeader = startHeader;
    while (currentHeader && currentLayer >= startLayer) {
        currentIndex++;
        currentHeader = getHeader(sheet, dim, currentIndex);
        currentLayer = currentHeader?.outlineLevel || 0;
    }
    const start = startIndex;
    const end = currentIndex - 1;
    const collapseFlagHeader = collapseFlagAfter
        ? getHeader(sheet, dim, end + 1)
        : getHeader(sheet, dim, start - 1);
    const isFolded = collapseFlagHeader?.collapsed || false;
    return { start: start - 1, end: end - 1, isFolded }; // -1 because indices start at 1 in excel and 0 in o-spreadsheet
}
function getHeader(sheet, dim, index) {
    return "COL" === dim
        ? sheet.cols.find((col) => col.min <= index && index <= col.max)
        : sheet.rows.find((row) => row.index === index);
}

/**
 * Convert the imported XLSX tables and pivots 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) {
        const sheet = convertedData.sheets.find((sheet) => sheet.name === xlsxSheet.sheetName);
        if (!sheet)
            continue;
        if (!sheet.tables)
            sheet.tables = [];
        for (const table of xlsxSheet.tables) {
            sheet.tables.push({ range: table.ref, config: convertTableConfig(table) });
        }
        for (const pivotTable of xlsxSheet.pivotTables) {
            sheet.tables.push({
                range: pivotTable.location.ref,
                config: convertPivotTableConfig(pivotTable),
            });
        }
    }
    convertTableFormulaReferences(convertedData.sheets, xlsxData.sheets);
}
function convertTableConfig(table) {
    const styleId = table.style?.name || "";
    return {
        hasFilters: table.autoFilter !== undefined,
        numberOfHeaders: table.headerRowCount,
        totalRow: table.totalsRowCount > 0,
        firstColumn: table.style?.showFirstColumn || false,
        lastColumn: table.style?.showLastColumn || false,
        bandedRows: table.style?.showRowStripes || false,
        bandedColumns: table.style?.showColumnStripes || false,
        styleId: TABLE_PRESETS[styleId] ? styleId : DEFAULT_TABLE_CONFIG.styleId,
    };
}
function convertPivotTableConfig(pivotTable) {
    return {
        hasFilters: false,
        numberOfHeaders: pivotTable.location.firstDataRow,
        totalRow: pivotTable.rowGrandTotals,
        firstColumn: true,
        lastColumn: pivotTable.style?.showLastColumn || false,
        bandedRows: pivotTable.style?.showRowStripes || false,
        bandedColumns: pivotTable.style?.showColStripes || false,
        styleId: DEFAULT_TABLE_CONFIG.styleId,
    };
}
/**
 * In all the sheets, replace the table-only references in the formula cells with standard references.
 */
function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
    let deconstructedSheets = null;
    for (const 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 (const table of tables) {
            for (const sheetId in deconstructedSheets) {
                const sheet = convertedSheets.find((s) => s.id === sheetId);
                for (const 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);
                    }
                    // sheet.cells[xc] = cellContent;
                }
            }
        }
    }
    if (!deconstructedSheets) {
        return;
    }
    for (const sheetId in deconstructedSheets) {
        const sheet = convertedSheets.find((s) => s.id === sheetId);
        for (const xc in deconstructedSheets[sheetId]) {
            const deconstructedCell = deconstructedSheets[sheetId][xc];
            if (deconstructedCell.length === 1) {
                sheet.cells[xc] = 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] = 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) {
    const deconstructedSheets = {};
    for (const sheet of convertedSheets) {
        for (const xc in sheet.cells) {
            const cellContent = sheet.cells[xc];
            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 CellErrorType.InvalidReference;
                    }
                    rowIndexes.push(tableZone.top);
                    break;
                case "#Totals":
                    if (!table.totalsRowCount) {
                        return CellErrorType.InvalidReference;
                    }
                    rowIndexes.push(tableZone.bottom);
                    break;
            }
        }
        else {
            const columns = part
                .split(":")
                .map((part) => part.trim())
                .map(removeBrackets);
            if (colIndexes.length) {
                return CellErrorType.InvalidReference;
            }
            const colRelativeIndex = table.cols.findIndex((col) => col.name === columns[0]);
            if (colRelativeIndex === -1) {
                return CellErrorType.InvalidReference;
            }
            colIndexes.push(colRelativeIndex + tableZone.left);
            if (columns[1]) {
                const colRelativeIndex2 = table.cols.findIndex((col) => col.name === columns[1]);
                if (colRelativeIndex2 === -1) {
                    return CellErrorType.InvalidReference;
                }
                colIndexes.push(colRelativeIndex2 + tableZone.left);
            }
        }
    }
    if (!areKeywordsCompatible(foundKeywords)) {
        return CellErrorType.InvalidReference;
    }
    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;
}

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

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

class XlsxCfExtractor extends XlsxBaseExtractor {
    theme;
    constructor(sheetFile, xlsxStructure, warningManager, theme) {
        super(sheetFile, xlsxStructure, warningManager);
        this.theme = theme;
    }
    extractConditionalFormattings() {
        const cfs = this.mapOnElements({ parent: this.rootFile.file.xml, query: "worksheet > conditionalFormatting" }, (cfElement) => {
            return {
                // sqref = ranges on which the cf applies, separated by spaces
                sqref: this.extractAttr(cfElement, "sqref", { required: true }).asString().split(" "),
                pivot: this.extractAttr(cfElement, "pivot")?.asBool(),
                cfRules: this.extractCFRules(cfElement, this.theme),
            };
        });
        // XLSX extension to OpenXml
        cfs.push(...this.mapOnElements({ parent: this.rootFile.file.xml, query: "extLst x14:conditionalFormatting" }, (cfElement) => {
            return {
                sqref: this.extractChildTextContent(cfElement, "xm:sqref", { required: true }).split(" "),
                pivot: this.extractAttr(cfElement, "xm:pivot")?.asBool(),
                cfRules: this.extractCFRules(cfElement, this.theme),
            };
        }));
        return cfs;
    }
    extractCFRules(cfElement, theme) {
        return this.mapOnElements({ parent: cfElement, query: "cfRule, x14:cfRule" }, (cfRuleElement) => {
            const cfType = this.extractAttr(cfRuleElement, "type", {
                required: true,
            }).asString();
            return {
                type: cfType,
                priority: this.extractAttr(cfRuleElement, "priority", { required: true }).asNum(),
                colorScale: this.extractCfColorScale(cfRuleElement, theme),
                formula: this.extractCfFormula(cfRuleElement),
                iconSet: this.extractCfIconSet(cfRuleElement),
                dxfId: this.extractAttr(cfRuleElement, "dxfId")?.asNum(),
                stopIfTrue: this.extractAttr(cfRuleElement, "stopIfTrue")?.asBool(),
                aboveAverage: this.extractAttr(cfRuleElement, "aboveAverage")?.asBool(),
                percent: this.extractAttr(cfRuleElement, "percent")?.asBool(),
                bottom: this.extractAttr(cfRuleElement, "bottom")?.asBool(),
                operator: this.extractAttr(cfRuleElement, "operator")?.asString(),
                text: this.extractAttr(cfRuleElement, "text")?.asString(),
                timePeriod: this.extractAttr(cfRuleElement, "timePeriod")?.asString(),
                rank: this.extractAttr(cfRuleElement, "rank")?.asNum(),
                stdDev: this.extractAttr(cfRuleElement, "stdDev")?.asNum(),
                equalAverage: this.extractAttr(cfRuleElement, "equalAverage")?.asBool(),
                dataBar: this.extractCfDataBar(cfRuleElement, theme),
            };
        });
    }
    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),
        };
    }
    extractCfDataBar(cfRulesElement, theme) {
        const dataBarElement = this.querySelector(cfRulesElement, "dataBar");
        if (!dataBarElement)
            return undefined;
        return {
            color: this.extractColor(dataBarElement.querySelector("color"), theme, "EFF7FF"),
            // TODO ATM, we only support color for dataBar, not the minimum and maximum fill percentage
            cfvos: this.extractCFVos(dataBarElement),
        };
    }
    extractCfIconSet(cfRulesElement) {
        const iconSetElement = this.querySelector(cfRulesElement, "iconSet, x14:iconSet");
        if (!iconSetElement)
            return undefined;
        return {
            iconSet: this.extractAttr(iconSetElement, "iconSet", {
                default: "3TrafficLights1",
            }).asString(),
            showValue: this.extractAttr(iconSetElement, "showValue", { default: true }).asBool(),
            percent: this.extractAttr(iconSetElement, "percent", { default: true }).asBool(),
            reverse: this.extractAttr(iconSetElement, "reverse")?.asBool(),
            custom: this.extractAttr(iconSetElement, "custom")?.asBool(),
            cfvos: this.extractCFVos(iconSetElement),
            cfIcons: this.extractCfIcons(iconSetElement),
        };
    }
    extractCfIcons(iconSetElement) {
        const icons = this.mapOnElements({ parent: iconSetElement, query: "cfIcon, x14:cfIcon" }, (cfIconElement) => {
            return {
                iconSet: this.extractAttr(cfIconElement, "iconSet", {
                    required: true,
                }).asString(),
                iconId: this.extractAttr(cfIconElement, "iconId", { required: true }).asNum(),
            };
        });
        return icons.length === 0 ? undefined : icons;
    }
    extractCFVos(parent) {
        return this.mapOnElements({ parent, query: "cfvo, x14:cfvo" }, (cfVoElement) => {
            return {
                type: this.extractAttr(cfVoElement, "type", {
                    required: true,
                }).asString(),
                gte: this.extractAttr(cfVoElement, "gte", { default: true })?.asBool(),
                value: cfVoElement.attributes["val"]
                    ? this.extractAttr(cfVoElement, "val")?.asString()
                    : this.extractChildTextContent(cfVoElement, "f, xm:f"),
            };
        });
    }
}

class XlsxDataValidationExtractor extends XlsxBaseExtractor {
    theme;
    constructor(sheetFile, xlsxStructure, warningManager, theme) {
        super(sheetFile, xlsxStructure, warningManager);
        this.theme = theme;
    }
    extractDataValidations() {
        const dataValidations = this.mapOnElements({ parent: this.rootFile.file.xml, query: "worksheet > dataValidations > dataValidation" }, (dvElement) => {
            return {
                type: this.extractAttr(dvElement, "type", { required: true }).asString(),
                operator: this.extractAttr(dvElement, "operator", {
                    default: "between",
                })?.asString(),
                sqref: this.extractAttr(dvElement, "sqref", { required: true }).asString().split(" "),
                errorStyle: this.extractAttr(dvElement, "errorStyle")?.asString(),
                formula1: this.extractDataValidationFormula(dvElement, 1)[0],
                formula2: this.extractDataValidationFormula(dvElement, 2)[0],
                showErrorMessage: this.extractAttr(dvElement, "showErrorMessage")?.asBool(),
                errorTitle: this.extractAttr(dvElement, "errorTitle")?.asString(),
                error: this.extractAttr(dvElement, "error")?.asString(),
                showInputMessage: this.extractAttr(dvElement, "showInputMessage")?.asBool(),
                promptTitle: this.extractAttr(dvElement, "promptTitle")?.asString(),
                prompt: this.extractAttr(dvElement, "prompt")?.asString(),
                allowBlank: this.extractAttr(dvElement, "allowBlank")?.asBool(),
            };
        });
        return dataValidations;
    }
    extractDataValidationFormula(dvElement, index) {
        return this.mapOnElements({ parent: dvElement, query: `formula${index}` }, (cfFormulaElements) => {
            return this.extractTextContent(cfFormulaElements, { required: true });
        });
    }
}

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}`);
            }
            if (CHART_TYPE_CONVERSION_MAP[chartType] === "combo") {
                return this.extractComboChart(rootChartElement);
            }
            // 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:chart > c:title a:t" }, (textElement) => {
                return textElement.textContent || "";
            }).join("");
            const barChartGrouping = this.extractChildAttr(rootChartElement, "c:grouping", "val", {
                default: "clustered",
            }).asString();
            const chartDirection = this.extractChildAttr(rootChartElement, "c:barDir", "val", {
                default: "col",
            }).asString();
            const chartHoleSize = this.extractChildAttr(rootChartElement, "c:holeSize", "val", {
                default: "0",
            }).asNum();
            return {
                title: { text: chartTitle },
                type: CHART_TYPE_CONVERSION_MAP[chartType],
                dataSets: this.extractChartDatasets(this.querySelectorAll(rootChartElement, `c:${chartType}`), chartType),
                labelRange: this.extractLabelRange(chartType, rootChartElement),
                backgroundColor: this.extractChildAttr(rootChartElement, "c:chartSpace > c:spPr a:srgbClr", "val", {
                    default: "ffffff",
                }).asString(),
                legendPosition: DRAWING_LEGEND_POSITION_CONVERSION_MAP[this.extractChildAttr(rootChartElement, "c:legendPos", "val", {
                    default: "none",
                }).asString()],
                stacked: barChartGrouping === "stacked",
                fontColor: "000000",
                horizontal: chartDirection === "bar",
                isDoughnut: chartHoleSize > 0,
                pieHolePercentage: chartHoleSize,
            };
        })[0];
    }
    extractLabelRange(chartType, rootChartElement) {
        if (chartType === "scatterChart") {
            return (this.extractChildTextContent(rootChartElement, `c:ser c:strRef c:f`) ||
                this.extractChildTextContent(rootChartElement, `c:ser c:numRef c:f`));
        }
        return this.extractChildTextContent(rootChartElement, `c:ser c:cat c:f`);
    }
    extractComboChart(chartElement) {
        // Title can be separated into multiple xml elements (for styling and such), we only import the text
        const chartTitle = this.mapOnElements({ parent: chartElement, query: "c:title a:t" }, (textElement) => {
            return textElement.textContent || "";
        }).join("");
        const barChartGrouping = this.extractChildAttr(chartElement, "c:grouping", "val", {
            default: "clustered",
        }).asString();
        return {
            title: { text: chartTitle },
            type: "combo",
            dataSets: [
                ...this.extractChartDatasets(this.querySelectorAll(chartElement, `c:barChart`), "comboChart"),
                ...this.extractChartDatasets(this.querySelectorAll(chartElement, `c:lineChart`), "comboChart"),
            ],
            labelRange: this.extractChildTextContent(chartElement, "c:ser c:cat c:f"),
            backgroundColor: this.extractChildAttr(chartElement, "c:chartSpace > c:spPr a:srgbClr", "val", {
                default: "ffffff",
            }).asString(),
            legendPosition: DRAWING_LEGEND_POSITION_CONVERSION_MAP[this.extractChildAttr(chartElement, "c:legendPos", "val", {
                default: "none",
            }).asString()],
            stacked: barChartGrouping === "stacked",
            fontColor: "000000",
        };
    }
    extractChartDatasets(chartElements, chartType) {
        return Array.from(chartElements)
            .map((element) => {
            if (chartType === "scatterChart") {
                return this.extractScatterChartDatasets(element);
            }
            return this.mapOnElements({ parent: element, query: "c:ser" }, (chartDataElement) => {
                let label = {};
                const reference = this.extractChildTextContent(chartDataElement, "c:tx c:f");
                if (reference) {
                    label = { reference };
                }
                else {
                    const text = this.extractChildTextContent(chartDataElement, "c:tx c:v");
                    if (text) {
                        label = { text };
                    }
                }
                const colorElements = this.querySelectorAll(chartDataElement, "c:spPr a:solidFill");
                const datasetColorElement = colorElements.length
                    ? this.querySelector(colorElements[0], "a:srgbClr")
                    : undefined;
                const color = datasetColorElement ? datasetColorElement.getAttribute("val") : undefined;
                return {
                    label,
                    range: this.extractChildTextContent(chartDataElement, "c:val c:f", {
                        required: true,
                    }),
                    backgroundColor: color && isColorValid(color) ? `${toHex(color)}` : undefined,
                    trend: this.extractChartTrendline(chartDataElement),
                };
            });
        })
            .flat();
    }
    extractChartTrendline(chartDataElement) {
        const trendlineElement = this.querySelector(chartDataElement, "c:trendline");
        if (!trendlineElement) {
            return undefined;
        }
        const trendlineType = this.extractChildAttr(trendlineElement, "c:trendlineType", "val");
        const trendlineColor = this.extractChildAttr(trendlineElement, "a:solidFill a:srgbClr", "val");
        return {
            type: trendlineType ? trendlineType.asString() : undefined,
            order: this.extractChildAttr(trendlineElement, "c:order", "val")?.asNum(),
            window: this.extractChildAttr(trendlineElement, "c:period", "val")?.asNum(),
            color: trendlineColor && isColorValid(trendlineColor.asString())
                ? `${toHex(trendlineColor.asString())}`
                : undefined,
        };
    }
    extractScatterChartDatasets(chartElement) {
        return this.mapOnElements({ parent: chartElement, query: "c:ser" }, (chartDataElement) => {
            let label = {};
            const colorElements = this.querySelectorAll(chartDataElement, "c:spPr a:solidFill a:srgbClr");
            const color = colorElements.length ? colorElements[0].getAttribute("val") : undefined;
            const reference = this.extractChildTextContent(chartDataElement, "c:tx c:f");
            if (reference) {
                label = { reference };
            }
            else {
                const text = this.extractChildTextContent(chartDataElement, "c:tx c:v");
                if (text) {
                    label = { text };
                }
            }
            return {
                label,
                range: this.extractChildTextContent(chartDataElement, "c:yVal c:f", { required: true }),
                trend: this.extractChartTrendline(chartDataElement),
                backgroundColor: color && isColorValid(color) ? `${toHex(color)}` : undefined,
            };
        });
    }
    /**
     * 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.");
        }
        let globalTag = undefined;
        for (const child of plotAreaElement.children) {
            const tag = removeTagEscapedNamespaces(child.tagName);
            if (XLSX_CHART_TYPES.some((chartType) => chartType === tag)) {
                if (!globalTag) {
                    globalTag = tag;
                }
                else if (globalTag !== tag) {
                    globalTag = "comboChart";
                }
            }
        }
        if (globalTag) {
            return globalTag;
        }
        throw new Error("Unknown chart type");
    }
}

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

/**
 * 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 {
                name: this.extractAttr(pivotElement, "name", { required: true }).asString(),
                rowGrandTotals: this.extractAttr(pivotElement, "rowGrandTotals", {
                    default: true,
                }).asBool(),
                location: this.extractPivotLocation(pivotElement),
                style: this.extractPivotStyleInfo(pivotElement),
            };
        })[0];
    }
    extractPivotLocation(pivotElement) {
        return this.mapOnElements({ query: "location", parent: pivotElement }, (pivotStyleElement) => {
            return {
                ref: this.extractAttr(pivotStyleElement, "ref", { required: true }).asString(),
                firstHeaderRow: this.extractAttr(pivotStyleElement, "firstHeaderRow", {
                    required: true,
                }).asNum(),
                firstDataRow: this.extractAttr(pivotStyleElement, "firstDataRow", {
                    required: true,
                }).asNum(),
                firstDataCol: this.extractAttr(pivotStyleElement, "firstDataCol", {
                    required: true,
                }).asNum(),
            };
        })[0];
    }
    extractPivotStyleInfo(pivotElement) {
        return this.mapOnElements({ query: "pivotTableStyleInfo", parent: pivotElement }, (pivotStyleElement) => {
            return {
                name: this.extractAttr(pivotStyleElement, "name", { required: true }).asString(),
                showRowHeaders: this.extractAttr(pivotStyleElement, "showRowHeaders", {
                    required: true,
                }).asBool(),
                showColHeaders: this.extractAttr(pivotStyleElement, "showColHeaders", {
                    required: true,
                }).asBool(),
                showRowStripes: this.extractAttr(pivotStyleElement, "showRowStripes", {
                    required: true,
                }).asBool(),
                showColStripes: this.extractAttr(pivotStyleElement, "showColStripes", {
                    required: true,
                }).asBool(),
                showLastColumn: this.extractAttr(pivotStyleElement, "showLastColumn")?.asBool(),
            };
        })[0];
    }
}

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

class XlsxSheetExtractor extends XlsxBaseExtractor {
    theme;
    constructor(sheetFile, xlsxStructure, warningManager, theme) {
        super(sheetFile, xlsxStructure, warningManager);
        this.theme = theme;
    }
    getSheet() {
        return this.mapOnElements({ query: "worksheet", parent: this.rootFile.file.xml }, (sheetElement) => {
            const sheetWorkbookInfo = this.getSheetWorkbookInfo();
            return {
                sheetName: this.extractSheetName(),
                sheetViews: this.extractSheetViews(sheetElement),
                sheetFormat: this.extractSheetFormat(sheetElement),
                sheetProperties: this.extractSheetProperties(sheetElement),
                cols: this.extractCols(sheetElement),
                rows: this.extractRows(sheetElement),
                sharedFormulas: this.extractSharedFormulas(sheetElement),
                merges: this.extractMerges(sheetElement),
                cfs: this.extractConditionalFormats(),
                dataValidations: this.extractDataValidations(),
                figures: this.extractFigures(sheetElement),
                hyperlinks: this.extractHyperLinks(sheetElement),
                tables: this.extractTables(sheetElement),
                pivotTables: this.extractPivotTables(),
                isVisible: sheetWorkbookInfo.state === "visible",
            };
        })[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 (const 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();
    }
    extractDataValidations() {
        return new XlsxDataValidationExtractor(this.rootFile, this.xlsxFileStructure, this.warningManager, this.theme).extractDataValidations();
    }
    extractFigures(worksheet) {
        const figures = this.mapOnElements({ parent: worksheet, query: "drawing" }, (drawingElement) => {
            const drawingId = this.extractAttr(drawingElement, "r:id", { required: true })?.asString();
            const drawingFile = this.getTargetXmlFile(this.relationships[drawingId]);
            return new XlsxFigureExtractor(drawingFile, this.xlsxFileStructure, this.warningManager).extractFigures();
        })[0];
        return figures || [];
    }
    extractTables(worksheet) {
        return this.mapOnElements({ query: "tablePart", parent: worksheet }, (tablePartElement) => {
            const tableId = this.extractAttr(tablePartElement, "r:id", { required: true })?.asString();
            const tableFile = this.getTargetXmlFile(this.relationships[tableId]);
            const tableExtractor = new XlsxTableExtractor(tableFile, this.xlsxFileStructure, this.warningManager);
            return tableExtractor.getTable();
        });
    }
    extractPivotTables() {
        try {
            return Object.values(this.relationships)
                .filter((relationship) => relationship.type.endsWith("pivotTable"))
                .map((pivotRelationship) => {
                const pivotFile = this.getTargetXmlFile(pivotRelationship);
                return new XlsxPivotExtractor(pivotFile, this.xlsxFileStructure, this.warningManager).getPivotTable();
            });
        }
        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(),
        };
    }
    extractSheetProperties(worksheet) {
        const propertiesElement = this.querySelector(worksheet, "sheetPr");
        if (!propertiesElement)
            return undefined;
        return {
            outlinePr: this.extractSheetOutlineProperties(propertiesElement),
            tabColor: this.extractColor(this.querySelector(propertiesElement, "tabColor"), this.theme),
        };
    }
    extractSheetOutlineProperties(sheetProperties) {
        const properties = this.querySelector(sheetProperties, "outlinePr");
        if (!properties)
            return undefined;
        return {
            summaryBelow: this.extractAttr(properties, "summaryBelow", { default: true }).asBool(),
            summaryRight: this.extractAttr(properties, "summaryRight", { default: true }).asBool(),
        };
    }
    extractCols(worksheet) {
        return this.mapOnElements({ parent: worksheet, query: "cols col" }, (colElement) => {
            return {
                width: this.extractAttr(colElement, "width")?.asNum(),
                customWidth: this.extractAttr(colElement, "customWidth")?.asBool(),
                bestFit: this.extractAttr(colElement, "bestFit")?.asBool(),
                hidden: this.extractAttr(colElement, "hidden")?.asBool(),
                min: this.extractAttr(colElement, "min", { required: true })?.asNum(),
                max: this.extractAttr(colElement, "max", { required: true })?.asNum(),
                styleIndex: this.extractAttr(colElement, "style")?.asNum(),
                outlineLevel: this.extractAttr(colElement, "outlineLevel")?.asNum(),
                collapsed: this.extractAttr(colElement, "collapsed")?.asBool(),
            };
        });
    }
    extractRows(worksheet) {
        const spilledCells = new Set();
        return this.mapOnElements({ parent: worksheet, query: "sheetData row" }, (rowElement) => {
            return {
                index: this.extractAttr(rowElement, "r", { required: true })?.asNum(),
                cells: this.extractCells(rowElement, spilledCells),
                height: this.extractAttr(rowElement, "ht")?.asNum(),
                customHeight: this.extractAttr(rowElement, "customHeight")?.asBool(),
                hidden: this.extractAttr(rowElement, "hidden")?.asBool(),
                styleIndex: this.extractAttr(rowElement, "s")?.asNum(),
                outlineLevel: this.extractAttr(rowElement, "outlineLevel")?.asNum(),
                collapsed: this.extractAttr(rowElement, "collapsed")?.asBool(),
            };
        });
    }
    extractCells(row, spilledCells) {
        return this.mapOnElements({ parent: row, query: "c" }, (cellElement) => {
            const xc = this.extractAttr(cellElement, "r", { required: true })?.asString();
            const formula = this.extractCellFormula(cellElement);
            if (formula?.ref && formula.sharedIndex === undefined) {
                const zone = toZone(formula.ref);
                for (const { col, row } of positions(zone)) {
                    const followerXc = toXC(col, row);
                    if (followerXc !== xc) {
                        spilledCells.add(followerXc);
                    }
                }
            }
            const isSpilled = spilledCells.has(xc);
            return {
                xc,
                styleIndex: this.extractAttr(cellElement, "s")?.asNum(),
                type: CELL_TYPE_CONVERSION_MAP[this.extractAttr(cellElement, "t", { default: "n" })?.asString()],
                value: isSpilled ? undefined : this.extractChildTextContent(cellElement, "v") ?? undefined,
                formula: isSpilled ? undefined : formula,
            };
        });
    }
    extractCellFormula(cellElement) {
        const formulaElement = this.querySelector(cellElement, "f");
        if (!formulaElement)
            return undefined;
        const content = this.extractTextContent(formulaElement);
        const sharedIndex = this.extractAttr(formulaElement, "si")?.asNum();
        const ref = this.extractAttr(formulaElement, "ref")?.asString();
        // This is the case of spilled cells of array formulas where <f> is empty
        if ((content === undefined || content.trim() === "") && sharedIndex === undefined) {
            return undefined;
        }
        return { content, sharedIndex, ref };
    }
    extractHyperLinks(worksheet) {
        return this.mapOnElements({ parent: worksheet, query: "hyperlink" }, (linkElement) => {
            const relId = this.extractAttr(linkElement, "r:id")?.asString();
            return {
                xc: this.extractAttr(linkElement, "ref", { required: true })?.asString(),
                location: this.extractAttr(linkElement, "location")?.asString(),
                display: this.extractAttr(linkElement, "display")?.asString(),
                relTarget: relId ? this.relationships[relId].target : undefined,
            };
        });
    }
    extractSharedFormulas(worksheet) {
        const sfElements = this.querySelectorAll(worksheet, `f[si][ref]`);
        const sfMap = {};
        for (const sfElement of sfElements) {
            const index = this.extractAttr(sfElement, "si", { required: true }).asNum();
            const formula = this.extractTextContent(sfElement, { required: true });
            sfMap[index] = formula;
        }
        const sfs = [];
        for (let i = 0; i < Object.keys(sfMap).length; i++) {
            if (!sfMap[i]) {
                this.warningManager.addParsingWarning(`Missing shared formula ${i}, replacing it by empty formula`);
                sfs.push("");
            }
            else {
                sfs.push(sfMap[i]);
            }
        }
        return sfs;
    }
}

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

class XlsxExternalBookExtractor extends XlsxBaseExtractor {
    getExternalBook() {
        return this.mapOnElements({ parent: this.rootFile.file.xml, query: "externalBook" }, (bookElement) => {
            return {
                rId: this.extractAttr(bookElement, "r:id", { required: true }).asString(),
                sheetNames: this.mapOnElements({ parent: bookElement, query: "sheetName" }, (sheetNameElement) => {
                    return this.extractAttr(sheetNameElement, "val", { required: true }).asString();
                }),
                datasets: this.extractExternalSheetData(bookElement),
            };
        })[0];
    }
    extractExternalSheetData(externalBookElement) {
        return this.mapOnElements({ parent: externalBookElement, query: "sheetData" }, (sheetDataElement) => {
            const cellsData = this.mapOnElements({ parent: sheetDataElement, query: "cell" }, (cellElement) => {
                return {
                    xc: this.extractAttr(cellElement, "r", { required: true }).asString(),
                    value: this.extractChildTextContent(cellElement, "v", { required: true }),
                };
            });
            const dataMap = {};
            for (const 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 (const 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 (const item of sheetItems) {
        const file = item?.attributes["PartName"].value;
        paths.push(file.substring(1)); // Remove the heading "/"
    }
    return paths;
}
/**
 * Get the corresponding relationship file for a given xml file in a XLSX directory.
 */
function getRelationFile(file, xmls) {
    if (file === CONTENT_TYPES_FILE) {
        return "_rels/.rels";
    }
    let relsFile = "";
    const pathParts = file.split("/");
    for (let i = 0; i < pathParts.length - 1; i++) {
        relsFile += pathParts[i] + "/";
    }
    relsFile += "_rels/";
    relsFile += pathParts[pathParts.length - 1] + ".rels";
    if (!xmls[relsFile]) {
        relsFile = undefined;
    }
    return relsFile;
}

const EXCEL_IMPORT_VERSION = "18.4.2";
class XlsxReader {
    warningManager;
    xmls;
    images;
    constructor(files) {
        this.warningManager = new XLSXImportWarningManager();
        this.xmls = {};
        this.images = [];
        for (const key of Object.keys(files)) {
            // Random files can be in xlsx (like a bin file for printer settings)
            if (key.endsWith(".xml") || key.endsWith(".rels")) {
                const contentString = escapeTagNamespaces(files[key]);
                this.xmls[key] = parseXML(new XMLString(contentString));
            }
            else if (key.includes("media/image")) {
                this.images.push({
                    fileName: key,
                    imageSrc: files[key]["imageSrc"],
                });
            }
        }
    }
    convertXlsx() {
        const xlsxData = this.getXlsxData();
        return this.convertImportedData(xlsxData);
    }
    // ---------------------------------------------------------------------------
    // 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,
            chartIds: [],
            imageIds: [],
        };
    }
    buildXlsxFileStructure() {
        const xlsxFileStructure = {
            sheets: getXLSXFilesOfType(CONTENT_TYPES.sheet, this.xmls),
            workbook: getXLSXFilesOfType(CONTENT_TYPES.workbook, this.xmls)[0] ||
                getXLSXFilesOfType(CONTENT_TYPES.macroEnabledWorkbook, this.xmls)[0] ||
                getXLSXFilesOfType(CONTENT_TYPES.templateWorkbook, this.xmls)[0] ||
                getXLSXFilesOfType(CONTENT_TYPES.macroEnabledTemplateWorkbook, this.xmls)[0] ||
                getXLSXFilesOfType(CONTENT_TYPES.excelAddInWorkbook, this.xmls)[0],
            styles: getXLSXFilesOfType(CONTENT_TYPES.styles, this.xmls)[0],
            sharedStrings: getXLSXFilesOfType(CONTENT_TYPES.sharedStrings, this.xmls)[0],
            theme: getXLSXFilesOfType(CONTENT_TYPES.themes, this.xmls)[0],
            charts: getXLSXFilesOfType(CONTENT_TYPES.chart, this.xmls),
            figures: getXLSXFilesOfType(CONTENT_TYPES.drawing, this.xmls),
            tables: getXLSXFilesOfType(CONTENT_TYPES.table, this.xmls),
            pivots: getXLSXFilesOfType(CONTENT_TYPES.pivot, this.xmls),
            externalLinks: getXLSXFilesOfType(CONTENT_TYPES.externalLink, this.xmls),
            images: this.images,
        };
        if (!xlsxFileStructure.workbook.rels) {
            throw Error(_t("Cannot find workbook relations file"));
        }
        return xlsxFileStructure;
    }
    // ---------------------------------------------------------------------------
    // Conversion
    // ---------------------------------------------------------------------------
    convertImportedData(data) {
        const convertedData = {
            version: EXCEL_IMPORT_VERSION,
            sheets: convertSheets(data, this.warningManager),
            styles: convertStyles(data, this.warningManager),
            formats: convertFormats(data, this.warningManager),
            borders: convertBorders(data, this.warningManager),
            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);
    const dependencies = [];
    const 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 };
}

// FIXME: Remove this map when Firefox supports getWeekInfo
// https:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getWeekInfo
const WEEK_START = {
    am_ET: 7,
    ar_001: 6,
    ar_SY: 6,
    az_AZ: 1,
    eu_ES: 1,
    be_BY: 1,
    bn_IN: 1,
    bs_BA: 1,
    bg_BG: 1,
    ca_ES: 1,
    zh_CN: 7,
    zh_HK: 7,
    zh_TW: 7,
    hr_HR: 1,
    cs_CZ: 1,
    da_DK: 1,
    nl_BE: 1,
    nl_NL: 1,
    en_AU: 7,
    en_CA: 7,
    en_GB: 1,
    en_IN: 7,
    en_NZ: 7,
    et_EE: 1,
    fi_FI: 1,
    fr_BE: 1,
    fr_CA: 7,
    fr_CH: 1,
    fr_FR: 1,
    gl_ES: 1,
    ka_GE: 1,
    de_DE: 1,
    de_CH: 1,
    el_GR: 1,
    gu_IN: 7,
    he_IL: 7,
    hi_IN: 7,
    hu_HU: 1,
    id_ID: 7,
    it_IT: 1,
    ja_JP: 7,
    kab_DZ: 6,
    km_KH: 7,
    ko_KP: 1,
    ko_KR: 7,
    lo_LA: 7,
    lv_LV: 1,
    lt_LT: 1,
    lb_LU: 1,
    mk_MK: 1,
    ml_IN: 1,
    mn_MN: 7,
    ms_MY: 1,
    nb_NO: 1,
    fa_IR: 6,
    pl_PL: 1,
    pt_AO: 1,
    pt_BR: 7,
    pt_PT: 1,
    ro_RO: 1,
    ru_RU: 1,
    sr_RS: 7,
    "sr@latin": 7,
    sk_SK: 1,
    sl_SI: 1,
    es_AR: 7,
    es_BO: 1,
    es_CL: 1,
    es_CO: 7,
    es_CR: 1,
    es_DO: 1,
    es_EC: 1,
    es_GT: 7,
    es_MX: 7,
    es_PA: 7,
    es_PE: 7,
    es_PY: 7,
    es_UY: 1,
    es_VE: 7,
    sw: 1,
    sv_SE: 1,
    th_TH: 7,
    tl_PH: 1,
    tr_TR: 1,
    uk_UA: 1,
    vi_VN: 1,
    sq_AL: 1,
    te_IN: 7,
    en_US: 7,
    my_MM: 7,
    es_ES: 1,
    es_419: 1,
};

const migrationStepRegistry = new Registry();
migrationStepRegistry
    .add("0.1", {
    // add the `activeSheet` field on data
    migrate(data) {
        if (data.sheets && data.sheets[0]) {
            data.activeSheet = data.sheets[0].name;
        }
        return data;
    },
})
    .add("0.2", {
    // add an id field in each sheet
    migrate(data) {
        if (data.sheets && data.sheets.length) {
            for (const sheet of data.sheets) {
                sheet.id = sheet.id || sheet.name;
            }
        }
        return data;
    },
})
    .add("0.3", {
    // activeSheet is now an id, not the name of a sheet
    migrate(data) {
        if (data.sheets && data.activeSheet) {
            const activeSheet = data.sheets.find((s) => s.name === data.activeSheet);
            data.activeSheet = activeSheet.id;
        }
        return data;
    },
})
    .add("0.4", {
    // add figures object in each sheets
    migrate(data) {
        for (const sheet of data.sheets || []) {
            sheet.figures = sheet.figures || [];
        }
        return data;
    },
})
    .add("0.5", {
    // 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
    migrate(data) {
        for (const sheet of data.sheets || []) {
            for (const xc in sheet.cells || []) {
                const cell = sheet.cells[xc];
                if (cell.content && cell.content.startsWith("=")) {
                    cell.formula = normalizeV9(cell.content);
                }
            }
        }
        return data;
    },
})
    .add("0.6", {
    // transform chart data structure
    migrate(data) {
        for (const sheet of data.sheets || []) {
            for (const f in sheet.figures || []) {
                const { dataSets, ...newData } = sheet.figures[f].data;
                const newDataSets = [];
                for (const 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;
    },
})
    .add("0.7", {
    // remove single quotes in sheet names
    migrate(data) {
        const namesTaken = [];
        for (const sheet of data.sheets || []) {
            if (!sheet.name) {
                continue;
            }
            const oldName = sheet.name;
            const escapedName = sanitizeSheetName(oldName, "_");
            const newName = getUniqueText(escapedName, namesTaken, {
                compute: (name, i) => `${name}${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 (const xc in sheet.cells) {
                const cell = sheet.cells[xc];
                if (cell.formula) {
                    cell.formula.dependencies = cell.formula.dependencies.map(replaceName);
                }
            }
            //charts
            for (const figure of sheet.figures || []) {
                if (figure.tag === "chart") {
                    const dataSets = figure.data.dataSets.map(replaceName);
                    const labelRange = replaceName(figure.data.labelRange);
                    figure.data = { ...figure.data, dataSets, labelRange };
                }
            }
            //ConditionalFormats
            for (const cf of sheet.conditionalFormats || []) {
                cf.ranges = cf.ranges.map(replaceName);
                for (const thresholdName of [
                    "minimum",
                    "maximum",
                    "midpoint",
                    "upperInflectionPoint",
                    "lowerInflectionPoint",
                ]) {
                    if (cf.rule[thresholdName]?.type === "formula") {
                        cf.rule[thresholdName].value = replaceName(cf.rule[thresholdName].value);
                    }
                }
            }
        }
        return data;
    },
})
    .add("0.8", {
    // transform chart data structure with design attributes
    migrate(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;
    },
})
    .add("0.9", {
    // de-normalize formula to reduce exported json size (~30%)
    migrate(data) {
        for (const sheet of data.sheets || []) {
            for (const xc in sheet.cells || []) {
                const cell = sheet.cells[xc];
                if (cell.formula) {
                    let { text, dependencies } = cell.formula;
                    for (const [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;
    },
})
    .add("0.10", {
    // normalize the formats of the cells
    migrate(data) {
        const formats = {};
        for (const sheet of data.sheets || []) {
            for (const xc in sheet.cells || []) {
                const cell = sheet.cells[xc];
                if (cell.format) {
                    cell.format = getItemId(cell.format, formats);
                }
            }
        }
        data.formats = formats;
        return data;
    },
})
    .add("15.4", {
    // Add isVisible to sheets
    migrate(data) {
        for (const sheet of data.sheets || []) {
            sheet.isVisible = true;
        }
        return data;
    },
})
    .add("15.4.1", {
    // Fix data filter duplication
    migrate(data) {
        return fixOverlappingFilters(data);
    },
})
    .add("16.3", {
    // Change Border description structure
    migrate(data) {
        for (const borderId in data.borders) {
            const border = data.borders[borderId];
            for (const position in border) {
                if (Array.isArray(border[position])) {
                    border[position] = {
                        style: border[position][0],
                        color: border[position][1],
                    };
                }
            }
        }
        return data;
    },
})
    .add("16.4", {
    // Add locale to spreadsheet settings
    migrate(data) {
        if (!data.settings) {
            data.settings = {};
        }
        if (!data.settings.locale) {
            data.settings.locale = DEFAULT_LOCALE;
        }
        return data;
    },
})
    .add("16.4.1", {
    // Fix datafilter duplication (post saas-17.1)
    migrate(data) {
        return fixOverlappingFilters(data);
    },
})
    .add("17.2", {
    // Rename filterTable to tables
    migrate(data) {
        for (const sheetData of data.sheets || []) {
            sheetData.tables = sheetData.tables || sheetData.filterTables || [];
            delete sheetData.filterTables;
        }
        return data;
    },
})
    .add("17.3", {
    // Add pivots
    migrate(data) {
        if (!data.pivots) {
            data.pivots = {};
        }
        if (!data.pivotNextId) {
            data.pivotNextId = getMaxObjectId(data.pivots) + 1;
        }
        return data;
    },
})
    .add("17.4", {
    // transform chart data structure (2)
    migrate(data) {
        for (const sheet of data.sheets || []) {
            for (const f in sheet.figures || []) {
                const figure = sheet.figures[f];
                if ("title" in figure.data && typeof figure.data.title === "string") {
                    figure.data.title = { text: figure.data.title };
                }
                const figureType = figure.data.type;
                if (!["line", "bar", "pie", "scatter", "waterfall", "combo"].includes(figureType)) {
                    continue;
                }
                const { dataSets, ...newData } = sheet.figures[f].data;
                const newDataSets = dataSets.map((dataRange) => ({ dataRange }));
                newData.dataSets = newDataSets;
                sheet.figures[f].data = newData;
            }
        }
        return data;
    },
})
    .add("18.0", {
    // Empty migration to allow external modules to add their own migration steps
    // before this version
    migrate(data) {
        return data;
    },
})
    .add("18.0.1", {
    // Change measures and dimensions `name` to `fieldName`
    // Add id to measures
    migrate(data) {
        for (const pivot of Object.values(data.pivots || {})) {
            pivot.measures = pivot.measures.map((measure) => ({
                id: measure.name, //Do not set name + aggregator, to support old formulas
                fieldName: measure.name,
                aggregator: measure.aggregator,
            }));
            pivot.columns = pivot.columns.map((column) => ({
                fieldName: column.name,
                order: column.order,
                granularity: column.granularity,
            }));
            pivot.rows = pivot.rows.map((row) => ({
                fieldName: row.name,
                order: row.order,
                granularity: row.granularity,
            }));
        }
        return data;
    },
})
    .add("18.0.2", {
    // "Add weekStart to locale",
    migrate(data) {
        const locale = data.settings?.locale;
        if (locale) {
            const code = locale.code;
            locale.weekStart = WEEK_START[code] || 1; // Default to Monday;
        }
        return data;
    },
})
    .add("18.0.3", {
    // group style and format into zones,
    migrate(data) {
        for (const sheet of data.sheets || []) {
            sheet.styles = {};
            sheet.formats = {};
            sheet.borders = {};
            for (const xc in sheet.cells) {
                sheet.styles[xc] = sheet.cells[xc].style;
                sheet.formats[xc] = sheet.cells[xc].format;
                sheet.borders[xc] = sheet.cells[xc].border;
                delete sheet.cells[xc].style;
                delete sheet.cells[xc].format;
                delete sheet.cells[xc].border;
            }
        }
        return data;
    },
})
    .add("18.0.4", {
    // "Add operator in gauge inflection points",
    migrate(data) {
        for (const sheet of data.sheets || []) {
            for (const figure of sheet.figures || []) {
                if (figure.tag !== "chart" || figure.data.type !== "gauge") {
                    continue;
                }
                const gaugeData = figure.data;
                if (gaugeData?.sectionRule?.lowerInflectionPoint) {
                    gaugeData.sectionRule.lowerInflectionPoint.operator = "<=";
                }
                if (gaugeData?.sectionRule?.upperInflectionPoint) {
                    gaugeData.sectionRule.upperInflectionPoint.operator = "<=";
                }
            }
        }
        return data;
    },
})
    .add("18.1", {
    // "tables are no longer inserted with filters by default",
    migrate(data) {
        for (const sheet of data.sheets || []) {
            for (const table of sheet.tables || []) {
                if (!table.config) {
                    table.config = { ...DEFAULT_TABLE_CONFIG, hasFilters: true };
                }
            }
        }
        return data;
    },
})
    .add("18.1.1", {
    // Flatten cell content: { content: "value" } -> "value"
    migrate(data) {
        for (const sheet of data.sheets || []) {
            for (const xc in sheet.cells) {
                const cell = sheet.cells[xc];
                if (cell) {
                    sheet.cells[xc] = cell.content;
                }
            }
        }
        return data;
    },
})
    .add("18.2", {
    // Empty migration to allow odoo migrate pivot custom sorting.
    migrate(data) {
        return data;
    },
})
    .add("18.3", {
    migrate(data) {
        if (!data.pivots) {
            return data;
        }
        for (const pivot of Object.values(data.pivots)) {
            if (!pivot.sortedColumn) {
                continue;
            }
            const measureIds = pivot.measures.map((measure) => measure.id);
            if (!measureIds.includes(pivot.sortedColumn.measure)) {
                delete pivot.sortedColumn;
            }
        }
        return data;
    },
})
    .add("18.3.1", {
    // Ensure fixed position, anchor and offset for existing figures based on current position
    migrate(data) {
        for (const sheet of data.sheets || []) {
            for (const figure of sheet.figures || []) {
                const figureData = figure;
                figure.offset = { x: figureData.x || 0, y: figureData.y || 0 };
                figure.col = 0;
                figure.row = 0;
                delete figure["x"];
                delete figure["y"];
            }
        }
        return data;
    },
})
    .add("18.4.1", {
    // Rename conditional format operators
    migrate(data) {
        const dvConversionMap = {
            textContains: "containsText",
            textNotContains: "notContainsText",
            textIs: "isEqualText",
            textIsEmail: "isEmail",
            textIsLink: "isLink",
        };
        const cfConversionMap = {
            BeginsWith: "beginsWithText",
            Between: "isBetween",
            ContainsText: "containsText",
            EndsWith: "endsWithText",
            Equal: "isEqual",
            GreaterThan: "isGreaterThan",
            GreaterThanOrEqual: "isGreaterOrEqualTo",
            IsEmpty: "isEmpty",
            IsNotEmpty: "isNotEmpty",
            LessThan: "isLessThan",
            LessThanOrEqual: "isLessOrEqualTo",
            NotBetween: "isNotBetween",
            NotContains: "notContainsText",
            NotEqual: "isNotEqual",
        };
        for (const sheet of data.sheets || []) {
            for (const cf of sheet.conditionalFormats || []) {
                if (cf.rule.type === "CellIsRule") {
                    cf.rule.operator = cfConversionMap[cf.rule.operator];
                }
            }
        }
        for (const sheet of data.sheets || []) {
            for (const dv of sheet.dataValidationRules || []) {
                if (dv.criterion.type in dvConversionMap) {
                    dv.criterion.type = dvConversionMap[dv.criterion.type];
                }
            }
        }
        return data;
    },
})
    .add("18.4.2", {
    migrate(data) {
        for (const sheet of data.sheets || []) {
            for (const figure of sheet.figures || []) {
                if (figure.tag !== "chart" || figure.data.type !== "scorecard") {
                    continue;
                }
                const scData = figure.data;
                if (scData.baselineDescr) {
                    scData.baselineDescr = { text: scData.baselineDescr };
                }
            }
        }
        return data;
    },
})
    .add("18.4.3", {
    migrate(data) {
        if (!data.pivots) {
            return data;
        }
        for (const pivotId in data.pivots) {
            const pivot = data.pivots[pivotId];
            if (pivot.sortedColumn) {
                const measure = pivot.measures.find((measure) => measure.fieldName === pivot.sortedColumn?.measure);
                if (measure) {
                    pivot.sortedColumn.measure = measure.id;
                }
            }
        }
        return data;
    },
})
    .add("18.5.1", {
    migrate(data) {
        for (const sheet of data.sheets || []) {
            for (const figure of sheet.figures || []) {
                if (figure.tag === "chart") {
                    figure.data.chartId = figure.id;
                }
            }
        }
        return data;
    },
});
function fixOverlappingFilters(data) {
    for (const sheet of data.sheets || []) {
        const knownDataFilterZones = [];
        for (const filterTable of sheet.filterTables || []) {
            const zone = toZone(filterTable.range);
            // See commit message of https://github.com/odoo/o-spreadsheet/pull/3632 of more 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;
}

/**
 * Represents the current version of the exported JSON data.
 * A new version must be created whenever a breaking change is introduced in the export format.
 * To define a new version, add an upgrade function to `migrationStepRegistry`.
 */
function getCurrentVersion() {
    return getSortedVersions().at(-1);
}
function getSortedVersions() {
    return migrationStepRegistry.getKeys().sort(compareVersions);
}
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();
    }
    console.debug("### Loading data ###");
    const start = performance.now();
    if (data["[Content_Types].xml"]) {
        const reader = new XlsxReader(data);
        data = reader.convertXlsx();
        if (verboseImport) {
            for (const parsingError of reader.warningManager.warnings.sort()) {
                console.warn(parsingError);
            }
        }
    }
    // apply migrations, if needed
    if ("version" in data) {
        if (isLegacyVersioning(data)) {
            data.version = LEGACY_VERSION_MAPPING[data.version];
        }
        if (data.version !== getCurrentVersion()) {
            console.debug("Migrating data from version", data.version);
            data = migrate(data);
        }
    }
    data = repairData(data);
    console.debug("Data loaded in", performance.now() - start, "ms");
    console.debug("###");
    return data;
}
const LEGACY_VERSION_MAPPING = {
    25: "18.2",
    24: "18.1.1",
    23: "18.1",
    22: "18.0.4",
    21: "18.0.3",
    20: "18.0.2",
    19: "18.0.1",
    18: "18.0",
    17: "17.4",
    16: "17.3",
    15: "17.2",
    "14.5": "16.4.1",
    14: "16.4",
    13: "16.3",
    "12.5": "15.4.1",
    12: "15.4",
    // not accurate starting at this point
    11: "0.10",
    10: "0.9",
    9: "0.8",
    8: "0.7",
    7: "0.6",
    6: "0.5",
    5: "0.4",
    4: "0.3",
    3: "0.2",
    2: "0.1",
    1: "0",
};
/**
 * Versions used to be an incremented integer.
 * This was later changed to match release versions (matching Odoo release names).
 */
function isLegacyVersioning(data) {
    return typeof data.version === "number";
}
// -----------------------------------------------------------------------------
// Migrations
// -----------------------------------------------------------------------------
function compareVersions(v1, v2) {
    const version1 = v1.split(".").map(Number);
    const version2 = v2.split(".").map(Number);
    for (let i = 0; i < Math.max(version1.length, version2.length); i++) {
        const part1 = version1[i] || 0;
        const part2 = version2[i] || 0;
        if (part1 > part2) {
            return 1;
        }
        if (part1 < part2) {
            return -1;
        }
    }
    return 0;
}
function migrate(data) {
    const start = performance.now();
    const versions = getSortedVersions();
    const index = versions.findIndex((v) => v === data.version);
    for (let i = index + 1; i < versions.length; i++) {
        const nextVersion = versions[i];
        data = migrationStepRegistry.get(nextVersion).migrate(data);
    }
    console.debug("Data migrated in", performance.now() - start, "ms");
    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 chartIds = 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);
            if (figure.tag === "chart") {
                if (chartIds.has(figure.data?.chartId)) {
                    figure.data.chartId += uuidGenerator.smallUuid();
                }
                chartIds.add(figure.data?.chartId);
            }
        }
    }
    data.uniqueFigureIds = true;
    return data;
}
/**
 * sanity check: try to fix missing fields/corrupted state by providing
 * sensible default values
 */
function setDefaults(partialData) {
    const data = Object.assign(createEmptyWorkbookData(), partialData, {
        version: getCurrentVersion(),
    });
    data.sheets = data.sheets
        ? data.sheets.map((s, i) => Object.assign(createEmptySheet(`Sheet${i + 1}`, `Sheet${i + 1}`), s))
        : [];
    if (data.sheets.length === 0) {
        data.sheets.push(createEmptySheet(INITIAL_SHEET_ID, "Sheet1"));
    }
    if (!isValidLocale(data.settings.locale)) {
        data.settings.locale = DEFAULT_LOCALE;
    }
    return data;
}
/**
 * The goal of this function is to repair corrupted/wrong initial messages caused by
 * a bug.
 * The bug should obviously be fixed, but it's too late for existing spreadsheet.
 */
function repairInitialMessages(data, initialMessages) {
    initialMessages = fixTranslatedSheetIds(data, initialMessages);
    initialMessages = dropCommands(initialMessages, "SORT_CELLS");
    initialMessages = dropCommands(initialMessages, "SET_DECIMAL");
    initialMessages = fixChartDefinitions(data, initialMessages);
    initialMessages = fixFigureOffset(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) {
    /**
     * Revisions created after version 18.5.1 contain the full chart definition in the command
     * if the data was alreay updated to 18.5.1, then those older revision cannot (by definition) be reaplied
     * and should not be replayed.
     * FIXME: every command should be versionned when upgraded to allow finer tuning.
     */
    if (!data.version || compareVersions(String(data.version), "18.5.1") >= 0) {
        return initialMessages;
    }
    const messages = [];
    const map = {};
    for (const sheet of data.sheets || []) {
        sheet.figures?.forEach((figure) => {
            if (figure.tag === "chart") {
                // chart definition
                map[figure.id] = figure.data;
            }
        });
    }
    for (const message of initialMessages) {
        if (message.type === "REMOTE_REVISION") {
            const commands = [];
            for (const cmd of message.commands) {
                let command = cmd;
                switch (cmd.type) {
                    case "CREATE_CHART":
                        map[cmd.chartId] = cmd.definition;
                        break;
                    case "UPDATE_CHART":
                        if (!map[cmd.chartId]) {
                            /** 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.chartId} not found.`);
                            continue;
                        }
                        const definition = map[cmd.chartId];
                        const newDefinition = { ...definition, ...cmd.definition };
                        command = { ...cmd, definition: newDefinition };
                        map[cmd.chartId] = newDefinition;
                        break;
                }
                commands.push(command);
            }
            messages.push({
                ...message,
                commands,
            });
        }
        else {
            messages.push(message);
        }
    }
    return messages;
}
function fixFigureOffset(data, messages) {
    const offset = {};
    for (const sheet of data.sheets || []) {
        sheet.figures?.forEach((figure) => {
            offset[figure.id] = figure.offset;
        });
    }
    for (const message of messages) {
        if (message.type === "REMOTE_REVISION") {
            for (const cmd of message.commands) {
                switch (cmd.type) {
                    case "UPDATE_FIGURE":
                        if (cmd.offset) {
                            if (cmd.offset.x === undefined) {
                                cmd.offset.x = offset[cmd.figureId] || 0;
                            }
                            if (cmd.offset.y === undefined) {
                                cmd.offset.y = offset[cmd.figureId] || 0;
                            }
                            offset[cmd.figureId] = offset;
                        }
                        break;
                    case "CREATE_IMAGE":
                    case "CREATE_CHART":
                    case "CREATE_FIGURE":
                        offset[cmd.figureId] = cmd.offset;
                        break;
                }
            }
        }
    }
    return messages;
}
function fixTranslatedDuplicateSheetName(data, initialMessages) {
    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 =
                            cmd.sheetNameTo ??
                                getDuplicateSheetName(sheetNames[cmd.sheetId], Object.values(sheetNames));
                        break;
                    case "CREATE_SHEET":
                        sheetNames[cmd.sheetId] = cmd.name || getNextSheetName(Object.values(sheetNames));
                        break;
                    case "RENAME_SHEET":
                        sheetNames[cmd.sheetId] = cmd.newName || 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: {},
        styles: {},
        formats: {},
        borders: {},
        cols: {},
        rows: {},
        merges: [],
        conditionalFormats: [],
        dataValidationRules: [],
        figures: [],
        tables: [],
        isVisible: true,
    };
}
function createEmptyWorkbookData(sheetName = "Sheet1") {
    return {
        version: getCurrentVersion(),
        sheets: [createEmptySheet(INITIAL_SHEET_ID, sheetName)],
        styles: {},
        formats: {},
        borders: {},
        revisionId: DEFAULT_REVISION_ID,
        uniqueFigureIds: true,
        settings: { locale: DEFAULT_LOCALE },
        pivots: {},
        pivotNextId: 1,
        customTableStyles: {},
    };
}
function createEmptyExcelSheet(sheetId, name) {
    return {
        ...createEmptySheet(sheetId, name),
        charts: [],
        images: [],
        cellValues: {},
    };
}
function createEmptyExcelWorkbookData() {
    return {
        ...createEmptyWorkbookData(),
        sheets: [createEmptyExcelSheet(INITIAL_SHEET_ID, "Sheet1")],
    };
}

const PasteInteractiveContent = {
    wrongPasteSelection: _t("This operation is not allowed with multiple selections."),
    willRemoveExistingMerge: RemoveDuplicateTerms.Errors.WillRemoveExistingMerge,
    wrongFigurePasteOption: _t("Cannot do a special paste of a figure."),
    frozenPaneOverlap: _t("This operation is not allowed due to an overlapping frozen pane."),
};
function handlePasteResult(env, result) {
    if (!result.isSuccessful) {
        if (result.reasons.includes("WrongPasteSelection" /* CommandResult.WrongPasteSelection */)) {
            env.raiseError(PasteInteractiveContent.wrongPasteSelection);
        }
        else if (result.reasons.includes("WillRemoveExistingMerge" /* CommandResult.WillRemoveExistingMerge */)) {
            env.raiseError(PasteInteractiveContent.willRemoveExistingMerge);
        }
        else if (result.reasons.includes("WrongFigurePasteOption" /* CommandResult.WrongFigurePasteOption */)) {
            env.raiseError(PasteInteractiveContent.wrongFigurePasteOption);
        }
        else if (result.reasons.includes("FrozenPaneOverlap" /* CommandResult.FrozenPaneOverlap */)) {
            env.raiseError(PasteInteractiveContent.frozenPaneOverlap);
        }
    }
}
function interactivePaste(env, target, pasteOption) {
    const result = env.model.dispatch("PASTE", { target, pasteOption });
    handlePasteResult(env, result);
}
async function interactivePasteFromOS(env, target, parsedClipboardContent, pasteOption) {
    let result;
    // We do not trust the clipboard content to be accurate and comprehensive.
    // Therefore, to ensure reliability, we handle unexpected errors that may
    // arise from content that would not be suitable for the current version.
    try {
        const clipboarContent = parsedClipboardContent;
        if (parsedClipboardContent.imageBlob) {
            try {
                const imageData = await env.imageProvider?.uploadFile(parsedClipboardContent.imageBlob);
                clipboarContent.imageData = imageData;
            }
            catch (e) {
                const msg = _t("An error occurred while uploading the image. %s", e.message);
                console.error(e);
                env.raiseError(msg);
            }
            delete parsedClipboardContent.imageBlob;
        }
        result = env.model.dispatch("PASTE_FROM_OS_CLIPBOARD", {
            target,
            clipboardContent: parsedClipboardContent,
            pasteOption,
        });
    }
    catch (error) {
        const parsedSpreadsheetContent = parsedClipboardContent.data;
        if (parsedSpreadsheetContent?.version !== getCurrentVersion()) {
            env.raiseError(_t("An unexpected error occurred while pasting content.\
          This is probably due to a spreadsheet version mismatch."));
        }
        else {
            env.raiseError(_t("An unexpected error occurred while pasting content.\
          Additional information can be found in the browser console."));
            console.error(error);
        }
        result = env.model.dispatch("PASTE_FROM_OS_CLIPBOARD", {
            target,
            clipboardContent: {
                text: parsedClipboardContent.text,
            },
            pasteOption,
        });
    }
    handlePasteResult(env, result);
}

/**
 * Create a table on the selected zone, with UI warnings to the user if the creation fails.
 * If a single cell is selected, expand the selection to non-empty adjacent cells to create a table.
 */
function interactiveCreateTable(env, sheetId, tableConfig = DEFAULT_TABLE_CONFIG) {
    let target = env.model.getters.getSelectedZones();
    let isDynamic = env.model.getters.canCreateDynamicTableOnZones(sheetId, target);
    if (target.length === 1 && !isDynamic && getZoneArea(target[0]) === 1) {
        env.model.selection.selectTableAroundSelection();
        target = env.model.getters.getSelectedZones();
        isDynamic = env.model.getters.canCreateDynamicTableOnZones(sheetId, target);
    }
    const ranges = target.map((zone) => env.model.getters.getRangeDataFromZone(sheetId, zone));
    const result = env.model.dispatch("CREATE_TABLE", {
        ranges,
        sheetId,
        config: tableConfig,
        tableType: isDynamic ? "dynamic" : "static",
    });
    if (result.isCancelledBecause("TableOverlap" /* CommandResult.TableOverlap */)) {
        env.raiseError(TableTerms.Errors.TableOverlap);
    }
    else if (result.isCancelledBecause("NonContinuousTargets" /* CommandResult.NonContinuousTargets */)) {
        env.raiseError(TableTerms.Errors.NonContinuousTargets);
    }
    return result;
}

//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
function setFormatter(env, format) {
    env.model.dispatch("SET_FORMATTING_WITH_PIVOT", {
        sheetId: env.model.getters.getActiveSheetId(),
        target: env.model.getters.getSelectedZones(),
        format,
    });
}
function setStyle(env, style) {
    env.model.dispatch("SET_FORMATTING", {
        sheetId: env.model.getters.getActiveSheetId(),
        target: env.model.getters.getSelectedZones(),
        style,
    });
}
//------------------------------------------------------------------------------
// Simple actions
//------------------------------------------------------------------------------
const PASTE_ACTION = async (env) => paste$1(env);
const PASTE_AS_VALUE_ACTION = async (env) => paste$1(env, "asValue");
async function paste$1(env, pasteOption) {
    const osClipboard = await env.clipboard.read();
    switch (osClipboard.status) {
        case "ok":
            const clipboardId = env.model.getters.getClipboardId();
            const osClipboardContent = parseOSClipboardContent(osClipboard.content);
            const osClipboardId = osClipboardContent.data?.clipboardId;
            const target = env.model.getters.getSelectedZones();
            if (clipboardId !== osClipboardId) {
                await interactivePasteFromOS(env, target, osClipboardContent, pasteOption);
            }
            else {
                interactivePaste(env, target, pasteOption);
            }
            if (env.model.getters.isCutOperation() && pasteOption !== "asValue") {
                await env.clipboard.write({ [ClipboardMIMEType.PlainText]: "" });
            }
            break;
        case "notImplemented":
            env.raiseError(_t("Pasting from the context menu is not supported in this browser. Use keyboard shortcuts ctrl+c / ctrl+v instead."));
            break;
        case "permissionDenied":
            env.raiseError(_t("Access to the clipboard denied by the browser. Please enable clipboard permission for this page in your browser settings."));
            break;
    }
}
const PASTE_FORMAT_ACTION = (env) => paste$1(env, "onlyFormat");
//------------------------------------------------------------------------------
// Grid manipulations
//------------------------------------------------------------------------------
const DELETE_CONTENT_ROWS_NAME = (env) => {
    if (env.model.getters.getSelectedZones().length > 1) {
        return _t("Clear rows");
    }
    let first;
    let last;
    const activesRows = env.model.getters.getActiveRows();
    if (activesRows.size !== 0) {
        first = largeMin([...activesRows]);
        last = largeMax([...activesRows]);
    }
    else {
        const zone = env.model.getters.getSelectedZones()[0];
        first = zone.top;
        last = zone.bottom;
    }
    if (first === last) {
        return _t("Clear row %s", (first + 1).toString());
    }
    return _t("Clear rows %s - %s", (first + 1).toString(), (last + 1).toString());
};
const DELETE_CONTENT_ROWS_ACTION = (env) => {
    const sheetId = env.model.getters.getActiveSheetId();
    const target = [...env.model.getters.getActiveRows()].map((index) => env.model.getters.getRowsZone(sheetId, index, index));
    env.model.dispatch("DELETE_CONTENT", {
        target,
        sheetId: env.model.getters.getActiveSheetId(),
    });
};
const DELETE_CONTENT_COLUMNS_NAME = (env) => {
    if (env.model.getters.getSelectedZones().length > 1) {
        return _t("Clear columns");
    }
    let first;
    let last;
    const activeCols = env.model.getters.getActiveCols();
    if (activeCols.size !== 0) {
        first = largeMin([...activeCols]);
        last = largeMax([...activeCols]);
    }
    else {
        const zone = env.model.getters.getSelectedZones()[0];
        first = zone.left;
        last = zone.right;
    }
    if (first === last) {
        return _t("Clear column %s", numberToLetters(first));
    }
    return _t("Clear columns %s - %s", numberToLetters(first), numberToLetters(last));
};
const DELETE_CONTENT_COLUMNS_ACTION = (env) => {
    const sheetId = env.model.getters.getActiveSheetId();
    const target = [...env.model.getters.getActiveCols()].map((index) => env.model.getters.getColsZone(sheetId, index, index));
    env.model.dispatch("DELETE_CONTENT", {
        target,
        sheetId: env.model.getters.getActiveSheetId(),
    });
};
const REMOVE_ROWS_NAME = (env) => {
    if (env.model.getters.getSelectedZones().length > 1) {
        return _t("Delete rows");
    }
    let first;
    let last;
    const activesRows = env.model.getters.getActiveRows();
    if (activesRows.size !== 0) {
        first = largeMin([...activesRows]);
        last = largeMax([...activesRows]);
    }
    else {
        const zone = env.model.getters.getSelectedZones()[0];
        first = zone.top;
        last = zone.bottom;
    }
    if (first === last) {
        return _t("Delete row %s", (first + 1).toString());
    }
    return _t("Delete rows %s - %s", (first + 1).toString(), (last + 1).toString());
};
const REMOVE_ROWS_ACTION = (env) => {
    const 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(),
        sheetName: env.model.getters.getActiveSheetName(),
        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 _t("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 _t("Delete column %s", numberToLetters(first));
    }
    return _t("Delete columns %s - %s", numberToLetters(first), numberToLetters(last));
};
const NOT_ALL_VISIBLE_ROWS_SELECTED = (env) => {
    const sheetId = env.model.getters.getActiveSheetId();
    const selectedRows = env.model.getters.getElementsFromSelection("ROW");
    return !env.model.getters.checkElementsIncludeAllVisibleHeaders(sheetId, "ROW", selectedRows);
};
const REMOVE_COLUMNS_ACTION = (env) => {
    const 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(),
        sheetName: env.model.getters.getActiveSheetName(),
        dimension: "COL",
        elements: columns,
    });
};
const NOT_ALL_VISIBLE_COLS_SELECTED = (env) => {
    const sheetId = env.model.getters.getActiveSheetId();
    const selectedCols = env.model.getters.getElementsFromSelection("COL");
    return !env.model.getters.checkElementsIncludeAllVisibleHeaders(sheetId, "COL", selectedCols);
};
const INSERT_ROWS_BEFORE_ACTION = (env) => {
    const activeRows = env.model.getters.getActiveRows();
    let row;
    let quantity;
    if (activeRows.size) {
        row = 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(),
        sheetName: env.model.getters.getActiveSheetName(),
        position: "before",
        base: row,
        quantity,
        dimension: "ROW",
    });
};
const INSERT_ROWS_AFTER_ACTION = (env) => {
    const activeRows = env.model.getters.getActiveRows();
    let row;
    let quantity;
    if (activeRows.size) {
        row = 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(),
        sheetName: env.model.getters.getActiveSheetName(),
        position: "after",
        base: row,
        quantity,
        dimension: "ROW",
    });
};
const INSERT_COLUMNS_BEFORE_ACTION = (env) => {
    const activeCols = env.model.getters.getActiveCols();
    let column;
    let quantity;
    if (activeCols.size) {
        column = 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(),
        sheetName: env.model.getters.getActiveSheetName(),
        position: "before",
        dimension: "COL",
        base: column,
        quantity,
    });
};
const INSERT_COLUMNS_AFTER_ACTION = (env) => {
    const activeCols = env.model.getters.getActiveCols();
    let column;
    let quantity;
    if (activeCols.size) {
        column = 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(),
        sheetName: env.model.getters.getActiveSheetName(),
        position: "after",
        dimension: "COL",
        base: column,
        quantity,
    });
};
const HIDE_COLUMNS_NAME = (env) => {
    const cols = env.model.getters.getElementsFromSelection("COL");
    const first = cols[0];
    const last = cols[cols.length - 1];
    if (cols.length === 1) {
        return _t("Hide column %s", numberToLetters(first).toString());
    }
    else if (last - first + 1 === cols.length) {
        return _t("Hide columns %s - %s", numberToLetters(first).toString(), numberToLetters(last).toString());
    }
    else {
        return _t("Hide columns");
    }
};
const HIDE_ROWS_NAME = (env) => {
    const rows = env.model.getters.getElementsFromSelection("ROW");
    const first = rows[0];
    const last = rows[rows.length - 1];
    if (rows.length === 1) {
        return _t("Hide row %s", (first + 1).toString());
    }
    else if (last - first + 1 === rows.length) {
        return _t("Hide rows %s - %s", (first + 1).toString(), (last + 1).toString());
    }
    else {
        return _t("Hide rows");
    }
};
//------------------------------------------------------------------------------
// Charts
//------------------------------------------------------------------------------
const CREATE_CHART = (env) => {
    const getters = env.model.getters;
    const figureId = env.model.uuidGenerator.smallUuid();
    const sheetId = getters.getActiveSheetId();
    let zones = getters.getSelectedZones();
    if (zones.length === 1 && getZoneArea(zones[0]) === 1) {
        env.model.selection.selectTableAroundSelection();
        zones = getters.getSelectedZones();
    }
    const size = { width: DEFAULT_FIGURE_WIDTH, height: DEFAULT_FIGURE_HEIGHT };
    const { col, row, offset } = centerFigurePosition(getters, size);
    const result = env.model.dispatch("CREATE_CHART", {
        sheetId,
        figureId,
        chartId: env.model.uuidGenerator.smallUuid(),
        col,
        row,
        offset,
        size,
        definition: getSmartChartDefinition(zones, env.model.getters),
    });
    if (result.isSuccessful) {
        env.model.dispatch("SELECT_FIGURE", { figureId });
        env.openSidePanel("ChartPanel");
    }
};
const CREATE_CAROUSEL = (env) => {
    const getters = env.model.getters;
    const figureId = env.model.uuidGenerator.smallUuid();
    const sheetId = getters.getActiveSheetId();
    const size = { width: DEFAULT_FIGURE_WIDTH, height: DEFAULT_FIGURE_HEIGHT };
    const { col, row, offset } = centerFigurePosition(getters, size);
    const result = env.model.dispatch("CREATE_CAROUSEL", {
        sheetId,
        figureId,
        col,
        row,
        offset,
        size,
        definition: { items: [] },
    });
    if (result.isSuccessful) {
        env.model.dispatch("SELECT_FIGURE", { figureId });
        env.openSidePanel("CarouselPanel", { figureId });
    }
};
//------------------------------------------------------------------------------
// Pivots
//------------------------------------------------------------------------------
const CREATE_PIVOT = (env) => {
    const pivotId = env.model.uuidGenerator.smallUuid();
    const newSheetId = env.model.uuidGenerator.smallUuid();
    const result = env.model.dispatch("INSERT_NEW_PIVOT", { pivotId, newSheetId });
    if (result.isSuccessful) {
        env.openSidePanel("PivotSidePanel", { pivotId });
    }
};
const REINSERT_DYNAMIC_PIVOT_CHILDREN = (env) => env.model.getters.getPivotIds().map((pivotId, index) => ({
    id: `reinsert_dynamic_pivot_${env.model.getters.getPivotFormulaId(pivotId)}`,
    name: env.model.getters.getPivotDisplayName(pivotId),
    sequence: index,
    execute: (env) => {
        const zone = env.model.getters.getSelectedZone();
        const table = env.model.getters.getPivot(pivotId).getCollapsedTableStructure().export();
        env.model.dispatch("INSERT_PIVOT_WITH_TABLE", {
            pivotId,
            table,
            col: zone.left,
            row: zone.top,
            sheetId: env.model.getters.getActiveSheetId(),
            pivotMode: "dynamic",
        });
        env.model.dispatch("REFRESH_PIVOT", { id: pivotId });
    },
    isVisible: (env) => env.model.getters.getPivot(pivotId).isValid(),
}));
const REINSERT_STATIC_PIVOT_CHILDREN = (env) => env.model.getters.getPivotIds().map((pivotId, index) => ({
    id: `reinsert_static_pivot_${env.model.getters.getPivotFormulaId(pivotId)}`,
    name: env.model.getters.getPivotDisplayName(pivotId),
    sequence: index,
    execute: (env) => {
        const zone = env.model.getters.getSelectedZone();
        const table = env.model.getters.getPivot(pivotId).getExpandedTableStructure();
        if (table.numberOfCells > PIVOT_MAX_NUMBER_OF_CELLS) {
            env.notifyUser({
                type: "warning",
                text: getPivotTooBigErrorMessage(table.numberOfCells, env.model.getters.getLocale()),
                sticky: true,
            });
            return;
        }
        env.model.dispatch("INSERT_PIVOT_WITH_TABLE", {
            pivotId,
            table: table.export(),
            col: zone.left,
            row: zone.top,
            sheetId: env.model.getters.getActiveSheetId(),
            pivotMode: "static",
        });
        env.model.dispatch("REFRESH_PIVOT", { id: pivotId });
    },
    isVisible: (env) => env.model.getters.getPivot(pivotId).isValid(),
}));
//------------------------------------------------------------------------------
// Image
//------------------------------------------------------------------------------
async function requestImage(env) {
    try {
        return await env.imageProvider.requestImage();
    }
    catch {
        env.raiseError(_t("An unexpected error occurred during the image transfer"));
        return;
    }
}
const CREATE_IMAGE = async (env) => {
    if (env.imageProvider) {
        const sheetId = env.model.getters.getActiveSheetId();
        const figureId = env.model.uuidGenerator.smallUuid();
        const image = await requestImage(env);
        if (!image) {
            return;
        }
        const size = getMaxFigureSize(env.model.getters, image.size);
        const { col, row, offset } = centerFigurePosition(env.model.getters, size);
        env.model.dispatch("CREATE_IMAGE", {
            sheetId,
            figureId,
            col,
            row,
            offset,
            size,
            definition: image,
        });
    }
};
//------------------------------------------------------------------------------
// Style/Format
//------------------------------------------------------------------------------
const FORMAT_PERCENT_ACTION = (env) => setFormatter(env, "0.00%");
//------------------------------------------------------------------------------
// Side panel
//------------------------------------------------------------------------------
const OPEN_CF_SIDEPANEL_ACTION = (env) => {
    env.openSidePanel("ConditionalFormatting", { selection: env.model.getters.getSelectedZones() });
};
const INSERT_LINK = (env) => {
    const { col, row } = env.model.getters.getActivePosition();
    env.getStore(CellPopoverStore).open({ col, row }, "LinkEditor");
};
const INSERT_LINK_NAME = (env) => {
    const sheetId = env.model.getters.getActiveSheetId();
    const { col, row } = env.model.getters.getActivePosition();
    const cell = env.model.getters.getEvaluatedCell({ sheetId, col, row });
    return cell && cell.link ? _t("Edit link") : _t("Insert link");
};
//------------------------------------------------------------------------------
// Filters action
//------------------------------------------------------------------------------
const SELECTED_TABLE_HAS_FILTERS = (env) => {
    const table = env.model.getters.getFirstTableInSelection();
    return table?.config.hasFilters || false;
};
const SELECTION_CONTAINS_SINGLE_TABLE = (env) => {
    const sheetId = env.model.getters.getActiveSheetId();
    const selectedZones = env.model.getters.getSelectedZones();
    return env.model.getters.getTablesOverlappingZones(sheetId, selectedZones).length === 1;
};
const IS_SELECTION_CONTINUOUS = (env) => {
    return areZonesContinuous(env.model.getters.getSelectedZones());
};
const ADD_DATA_FILTER = (env) => {
    const sheetId = env.model.getters.getActiveSheetId();
    const table = env.model.getters.getFirstTableInSelection();
    if (table) {
        env.model.dispatch("UPDATE_TABLE", {
            sheetId,
            zone: table.range.zone,
            config: { hasFilters: true },
        });
    }
    else {
        const tableConfig = {
            ...DEFAULT_TABLE_CONFIG,
            hasFilters: true,
            bandedRows: false,
            styleId: "TableStyleLight11",
        };
        interactiveCreateTable(env, sheetId, tableConfig);
    }
};
const REMOVE_DATA_FILTER = (env) => {
    const sheetId = env.model.getters.getActiveSheetId();
    const table = env.model.getters.getFirstTableInSelection();
    if (!table) {
        return;
    }
    env.model.dispatch("UPDATE_TABLE", {
        sheetId,
        zone: table.range.zone,
        config: { hasFilters: false },
    });
};
const INSERT_TABLE = (env) => {
    const sheetId = env.model.getters.getActiveSheetId();
    const result = interactiveCreateTable(env, sheetId);
    if (result.isSuccessful) {
        env.openSidePanel("TableSidePanel", {});
    }
};
const DELETE_SELECTED_TABLE = (env) => {
    const position = env.model.getters.getActivePosition();
    const table = env.model.getters.getTable(position);
    if (!table) {
        return;
    }
    env.model.dispatch("REMOVE_TABLE", {
        sheetId: position.sheetId,
        target: [table.range.zone],
    });
};
//------------------------------------------------------------------------------
// Sorting action
//------------------------------------------------------------------------------
const IS_ONLY_ONE_RANGE = (env) => {
    return env.model.getters.getSelectedZones().length === 1;
};
const CAN_INSERT_HEADER = (env, dimension) => {
    if (!IS_ONLY_ONE_RANGE(env)) {
        return false;
    }
    const activeHeaders = dimension === "COL" ? env.model.getters.getActiveCols() : env.model.getters.getActiveRows();
    const ortogonalActiveHeaders = dimension === "COL" ? env.model.getters.getActiveRows() : env.model.getters.getActiveCols();
    const sheetId = env.model.getters.getActiveSheetId();
    const zone = env.model.getters.getSelectedZone();
    const allSheetSelected = isEqual(zone, env.model.getters.getSheetZone(sheetId));
    return isConsecutive(activeHeaders) && (ortogonalActiveHeaders.size === 0 || allSheetSelected);
};
const CREATE_OR_REMOVE_FILTER_ACTION = {
    name: (env) => SELECTED_TABLE_HAS_FILTERS(env) ? _t("Remove selected filters") : _t("Add filters"),
    isEnabled: (env) => IS_SELECTION_CONTINUOUS(env),
    execute: (env) => SELECTED_TABLE_HAS_FILTERS(env) ? REMOVE_DATA_FILTER(env) : ADD_DATA_FILTER(env),
    icon: "o-spreadsheet-Icon.FILTER_ICON_ACTIVE",
};

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

class PositionMap {
    map = {};
    constructor(entries = []) {
        for (const [position, value] of entries) {
            this.set(position, value);
        }
    }
    set({ sheetId, col, row }, value) {
        const map = this.map;
        if (!map[sheetId]) {
            map[sheetId] = {};
        }
        if (!map[sheetId][col]) {
            map[sheetId][col] = {};
        }
        map[sheetId][col][row] = value;
    }
    setMany(values) {
        for (const [position, value] of values) {
            this.set(position, value);
        }
    }
    get({ sheetId, col, row }) {
        return this.map[sheetId]?.[col]?.[row];
    }
    getSheet(sheetId) {
        return this.map[sheetId];
    }
    clearSheet(sheetId) {
        delete this.map[sheetId];
    }
    has({ sheetId, col, row }) {
        return this.map[sheetId]?.[col]?.[row] !== undefined;
    }
    delete({ sheetId, col, row }) {
        delete this.map[sheetId]?.[col]?.[row];
    }
    keys() {
        const map = this.map;
        const keys = [];
        for (const sheetId in map) {
            for (const col in map[sheetId]) {
                for (const row in map[sheetId][col]) {
                    keys.push({ sheetId, col: parseInt(col), row: parseInt(row) });
                }
            }
        }
        return keys;
    }
    keysForSheet(sheetId) {
        const map = this.map[sheetId];
        if (!map) {
            return [];
        }
        const keys = [];
        for (const col in map) {
            for (const row in map[col]) {
                keys.push({ sheetId, col: parseInt(col), row: parseInt(row) });
            }
        }
        return keys;
    }
    *entries() {
        const map = this.map;
        for (const position of this.keys()) {
            const { sheetId, col, row } = position;
            yield [position, map[sheetId][col][row]];
        }
    }
}

class FormulaFingerprintStore extends SpreadsheetStore {
    mutators = ["enable", "disable"];
    isInvalidated = false;
    fingerprintColors = {
        [DATA_FINGERPRINT]: "#D9D9D9",
    };
    isEnabled = false;
    colors = new PositionMap();
    handle(cmd) {
        if (isCoreCommand(cmd) && this.isEnabled) {
            this.isInvalidated = true;
        }
        switch (cmd.type) {
            case "UNDO":
            case "REDO":
            case "ACTIVATE_SHEET":
                if (this.isEnabled) {
                    this.isInvalidated = true;
                }
                break;
        }
    }
    finalize() {
        if (this.isInvalidated) {
            this.isInvalidated = false;
            this.computeFingerprints();
        }
    }
    enable() {
        this.isEnabled = true;
        this.computeFingerprints();
    }
    disable() {
        this.isEnabled = false;
        this.colors = new PositionMap();
    }
    computeFingerprints() {
        this.colors = new PositionMap();
        const fingerprints = new PositionMap();
        const allFingerprints = new Set();
        const activeSheetId = this.getters.getActiveSheetId();
        const cells = this.getters.getCells(activeSheetId);
        for (const cellId in cells) {
            const fingerprint = this.computeFingerprint(cells[cellId]);
            if (!fingerprint) {
                continue;
            }
            allFingerprints.add(fingerprint);
            const position = this.getters.getCellPosition(cellId);
            fingerprints.set(position, fingerprint);
        }
        this.assignColors(allFingerprints);
        for (const [position, fingerprint] of fingerprints.entries()) {
            const color = this.fingerprintColors[fingerprint];
            this.colors.set(position, color);
            this.colorSpreadZone(position, color);
        }
    }
    colorSpreadZone(position, fingerprintColor) {
        const spreadZone = this.getters.getSpreadZone(position);
        if (!spreadZone) {
            return;
        }
        const sheetId = position.sheetId;
        for (let row = spreadZone.top; row <= spreadZone.bottom; row++) {
            for (let col = spreadZone.left; col <= spreadZone.right; col++) {
                const spreadPosition = { sheetId, col, row };
                this.colors.set(spreadPosition, fingerprintColor);
            }
        }
    }
    assignColors(fingerprints) {
        const colors = new AlternatingColorGenerator(fingerprints.size);
        Object.keys(this.fingerprintColors).forEach(() => colors.next());
        for (const fingerprint of fingerprints) {
            if (!this.fingerprintColors[fingerprint]) {
                this.fingerprintColors[fingerprint] = setColorAlpha(colors.next(), 0.8);
            }
        }
    }
    computeFingerprint(cell) {
        const position = this.getters.getCellPosition(cell.id);
        if (cell.isFormula) {
            return this.computeFormulaFingerprint(position, cell);
        }
        else {
            return this.getLiteralFingerprint(position);
        }
    }
    computeFormulaFingerprint(position, cell) {
        const dependencies = cell.compiledFormula.dependencies;
        const colCellOffset = position.col;
        const rowCellOffset = position.row;
        const positionSheetIndex = this.getters.getSheetIds().indexOf(position.sheetId);
        // As an optimization, we do not build each reference vector individually
        // to sum them up, but instead we directly add each component to the resulting
        // vector. This is equivalent to summing up all reference vectors.
        const fingerprintVector = {
            dx: 0,
            dy: 0,
            dSheet: 0,
        };
        for (const range of dependencies) {
            const zone = range.zone;
            const [left, right] = range.parts;
            const rangeSheetIndex = this.getters.getSheetIds().indexOf(range.sheetId);
            fingerprintVector.dSheet = rangeSheetIndex - positionSheetIndex;
            // in relative mode, we offset the col and row by the cell's position
            // in absolute mode, we offset the col and row relative to the sheet
            const isLeftUnbounded = isFullRowRange(range) && !range.unboundedZone.hasHeader;
            const isTopUnbounded = isFullColRange(range) && !range.unboundedZone.hasHeader;
            const leftOffset = isLeftUnbounded || left?.colFixed ? 0 : colCellOffset;
            const topOffset = isTopUnbounded || left?.rowFixed ? 0 : rowCellOffset;
            const isRightFixed = (!right && left?.colFixed) || right?.colFixed;
            const isBottomFixed = (!right && left?.rowFixed) || right?.rowFixed;
            const isRightUnbounded = range.unboundedZone.right === undefined;
            const isBottomUnbounded = range.unboundedZone.bottom === undefined;
            const rightOffset = isRightUnbounded || isRightFixed ? 0 : colCellOffset;
            const bottomOffset = isBottomUnbounded || isBottomFixed ? 0 : rowCellOffset;
            const referenceZone = reorderZone({
                top: zone.top - topOffset,
                left: zone.left - leftOffset,
                right: zone.right - rightOffset,
                bottom: zone.bottom - bottomOffset,
            });
            for (let dy = referenceZone.top; dy <= referenceZone.bottom; dy++) {
                for (let dx = referenceZone.left; dx <= referenceZone.right; dx++) {
                    fingerprintVector.dx += dx;
                    fingerprintVector.dy += dy;
                }
            }
        }
        const normalizedFormula = cell.compiledFormula.normalizedFormula;
        return hash(fingerprintVector) + normalizedFormula;
    }
    getLiteralFingerprint(position) {
        const evaluatedCell = this.getters.getEvaluatedCell(position);
        switch (evaluatedCell.type) {
            case CellValueType.number:
            case CellValueType.boolean:
                return DATA_FINGERPRINT;
            case CellValueType.text:
            case CellValueType.empty:
            case CellValueType.error:
                return undefined;
        }
    }
}
function hash(vector) {
    return Object.entries(vector)
        .sort(([a], [b]) => a.localeCompare(b))
        .map(([_, value]) => value)
        .join(",");
}
const DATA_FINGERPRINT = "DATA_FINGERPRINT";

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

const TABLE_ELEMENTS_BY_PRIORITY = [
    "wholeTable",
    "firstColumnStripe",
    "secondColumnStripe",
    "firstRowStripe",
    "secondRowStripe",
    "firstColumn",
    "lastColumn",
    "headerRow",
    "totalRow",
];
/** Return the content zone of the table, ie. the table zone without the headers */
function getTableContentZone(tableZone, tableConfig) {
    const numberOfHeaders = tableConfig.numberOfHeaders;
    const contentZone = { ...tableZone, top: tableZone.top + numberOfHeaders };
    return contentZone.top <= contentZone.bottom ? contentZone : undefined;
}
function getTableTopLeft(table) {
    const range = table.range;
    return { row: range.zone.top, col: range.zone.left, sheetId: range.sheetId };
}
function createFilter(id, range, config, createRange) {
    const zone = range.zone;
    if (zone.left !== zone.right) {
        throw new Error("Can only define a filter on a single column");
    }
    const filteredZone = { ...zone, top: zone.top + config.numberOfHeaders };
    const filteredRange = createRange(range.sheetId, filteredZone);
    return {
        id,
        rangeWithHeaders: range,
        col: zone.left,
        filteredRange: filteredZone.top > filteredZone.bottom ? undefined : filteredRange,
    };
}
function isStaticTable(table) {
    return table.type === "static" || table.type === "forceStatic";
}
function getComputedTableStyle(tableConfig, style, numberOfCols, numberOfRows) {
    return {
        borders: getAllTableBorders(tableConfig, style, numberOfCols, numberOfRows),
        styles: getAllTableStyles(tableConfig, style, numberOfCols, numberOfRows),
    };
}
function getAllTableBorders(tableConfig, style, nOfCols, nOfRows) {
    const borders = generateMatrix(nOfCols, nOfRows, () => ({}));
    for (const tableElement of TABLE_ELEMENTS_BY_PRIORITY) {
        const styleBorder = style[tableElement]?.border;
        if (!styleBorder)
            continue;
        const zones = getTableElementZones(tableElement, tableConfig, nOfCols, nOfRows);
        for (const zone of zones) {
            for (let col = zone.left; col <= zone.right; col++) {
                for (let row = zone.top; row <= zone.bottom; row++) {
                    // Special case: we don't want borders inside the headers rows
                    const noInsideBorder = tableElement === "wholeTable" && row <= tableConfig.numberOfHeaders - 1;
                    if (row === zone.top && styleBorder?.top) {
                        setBorderDescr(borders, "top", styleBorder.top, col, row, nOfCols, nOfRows);
                    }
                    else if (row !== zone.top && !noInsideBorder && styleBorder?.horizontal) {
                        setBorderDescr(borders, "top", styleBorder.horizontal, col, row, nOfCols, nOfRows);
                    }
                    if (row === zone.bottom && styleBorder?.bottom) {
                        setBorderDescr(borders, "bottom", styleBorder.bottom, col, row, nOfCols, nOfRows);
                    }
                    if (col === zone.left && styleBorder?.left) {
                        setBorderDescr(borders, "left", styleBorder.left, col, row, nOfCols, nOfRows);
                    }
                    if (col === zone.right && styleBorder?.right) {
                        setBorderDescr(borders, "right", styleBorder.right, col, row, nOfCols, nOfRows);
                    }
                    else if (col !== zone.right && !noInsideBorder && styleBorder?.vertical) {
                        setBorderDescr(borders, "right", styleBorder.vertical, col, row, nOfCols, nOfRows);
                    }
                }
            }
        }
    }
    return borders;
}
/**
 * Set the border description for a given border direction (top, bottom, left, right) in the computedBorders array.
 * Also set the corresponding borders of adjacent cells (eg. if the border is set on the top of a cell, the bottom
 * border of the cell above is set).
 */
function setBorderDescr(computedBorders, dir, borderDescr, col, row, numberOfCols, numberOfRows) {
    switch (dir) {
        case "top":
            computedBorders[col][row].top = borderDescr;
            if (row !== 0) {
                computedBorders[col][row - 1].bottom = borderDescr;
            }
            return;
        case "bottom":
            computedBorders[col][row].bottom = borderDescr;
            if (row !== numberOfRows - 1) {
                computedBorders[col][row + 1].top = borderDescr;
            }
            return;
        case "left":
            computedBorders[col][row].left = borderDescr;
            if (col !== 0) {
                computedBorders[col - 1][row].right = borderDescr;
            }
            return;
        case "right":
            computedBorders[col][row].right = borderDescr;
            if (col !== numberOfCols - 1) {
                computedBorders[col + 1][row].left = borderDescr;
            }
            return;
    }
}
function getAllTableStyles(tableConfig, style, numberOfCols, numberOfRows) {
    const styles = generateMatrix(numberOfCols, numberOfRows, () => ({}));
    for (const tableElement of TABLE_ELEMENTS_BY_PRIORITY) {
        const tableElStyle = style[tableElement];
        const bold = isTableElementInBold(tableElement);
        if (!tableElStyle && !bold) {
            continue;
        }
        const zones = getTableElementZones(tableElement, tableConfig, numberOfCols, numberOfRows);
        for (const zone of zones) {
            for (let col = zone.left; col <= zone.right; col++) {
                for (let row = zone.top; row <= zone.bottom; row++) {
                    if (!styles[col][row]) {
                        styles[col][row] = {};
                    }
                    styles[col][row] = {
                        ...styles[col][row],
                        ...tableElStyle?.style,
                    };
                    if (bold) {
                        styles[col][row].bold = true;
                    }
                }
            }
        }
    }
    return styles;
}
function isTableElementInBold(tableElement) {
    return (tableElement === "firstColumn" ||
        tableElement === "lastColumn" ||
        tableElement === "headerRow" ||
        tableElement === "totalRow");
}
function getTableElementZones(el, tableConfig, numberOfCols, numberOfRows) {
    const zones = [];
    const headerRows = Math.min(tableConfig.numberOfHeaders, numberOfRows);
    const totalRows = tableConfig.totalRow ? 1 : 0;
    const lastCol = numberOfCols - 1;
    const lastRow = numberOfRows - 1;
    switch (el) {
        case "wholeTable":
            zones.push({ top: 0, left: 0, bottom: lastRow, right: lastCol });
            break;
        case "firstColumn":
            if (!tableConfig.firstColumn)
                break;
            zones.push({ top: 0, left: 0, bottom: lastRow, right: 0 });
            break;
        case "lastColumn":
            if (!tableConfig.lastColumn)
                break;
            zones.push({ top: 0, left: lastCol, bottom: lastRow, right: lastCol });
            break;
        case "headerRow":
            if (!tableConfig.numberOfHeaders)
                break;
            zones.push({ top: 0, left: 0, bottom: headerRows - 1, right: lastCol });
            break;
        case "totalRow":
            if (!tableConfig.totalRow)
                break;
            zones.push({ top: lastRow, left: 0, bottom: lastRow, right: lastCol });
            break;
        case "firstRowStripe":
            if (!tableConfig.bandedRows)
                break;
            for (let i = headerRows; i < numberOfRows - totalRows; i += 2) {
                zones.push({ top: i, left: 0, bottom: i, right: lastCol });
            }
            break;
        case "secondRowStripe":
            if (!tableConfig.bandedRows)
                break;
            for (let i = headerRows + 1; i < numberOfRows - totalRows; i += 2) {
                zones.push({ top: i, left: 0, bottom: i, right: lastCol });
            }
            break;
        case "firstColumnStripe":
            if (!tableConfig.bandedColumns)
                break;
            for (let i = 0; i < numberOfCols; i += 2) {
                zones.push({ top: headerRows, left: i, bottom: lastRow - totalRows, right: i });
            }
            break;
        case "secondColumnStripe":
            if (!tableConfig.bandedColumns)
                break;
            for (let i = 1; i < numberOfCols; i += 2) {
                zones.push({ top: headerRows, left: i, bottom: lastRow - totalRows, right: i });
            }
            break;
    }
    return zones;
}

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

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

const undo = {
    name: _t("Undo"),
    description: "Ctrl+Z",
    execute: (env) => env.model.dispatch("REQUEST_UNDO"),
    isEnabled: (env) => env.model.getters.canUndo(),
    icon: "o-spreadsheet-Icon.UNDO",
};
const redo = {
    name: _t("Redo"),
    description: "Ctrl+Y",
    execute: (env) => env.model.dispatch("REQUEST_REDO"),
    isEnabled: (env) => env.model.getters.canRedo(),
    icon: "o-spreadsheet-Icon.REDO",
};
const copy = {
    name: _t("Copy"),
    description: "Ctrl+C",
    isReadonlyAllowed: true,
    execute: async (env) => {
        env.model.dispatch("COPY");
        await env.clipboard.write(await env.model.getters.getClipboardTextAndImageContent());
    },
    icon: "o-spreadsheet-Icon.CLIPBOARD",
};
const cut = {
    name: _t("Cut"),
    description: "Ctrl+X",
    execute: async (env) => {
        interactiveCut(env);
        await env.clipboard.write(await env.model.getters.getClipboardTextAndImageContent());
    },
    icon: "o-spreadsheet-Icon.CUT",
};
const paste = {
    name: _t("Paste"),
    description: "Ctrl+V",
    execute: PASTE_ACTION,
    icon: "o-spreadsheet-Icon.PASTE",
};
const pasteSpecial = {
    name: _t("Paste special"),
    isVisible: (env) => {
        return !env.model.getters.isCutOperation();
    },
    icon: "o-spreadsheet-Icon.PASTE",
};
const pasteSpecialValue = {
    name: _t("Paste as value"),
    description: "Ctrl+Shift+V",
    execute: PASTE_AS_VALUE_ACTION,
};
const pasteSpecialFormat = {
    name: _t("Paste format only"),
    execute: PASTE_FORMAT_ACTION,
};
const findAndReplace = {
    name: _t("Find and replace"),
    description: "Ctrl+H",
    isReadonlyAllowed: true,
    execute: (env) => {
        env.openSidePanel("FindAndReplace", {});
    },
    isEnabled: (env) => !env.isSmall,
    icon: "o-spreadsheet-Icon.SEARCH",
};
const deleteValues = {
    name: _t("Delete values"),
    execute: (env) => env.model.dispatch("DELETE_UNFILTERED_CONTENT", {
        sheetId: env.model.getters.getActiveSheetId(),
        target: env.model.getters.getSelectedZones(),
    }),
};
const deleteRows = {
    name: REMOVE_ROWS_NAME,
    execute: REMOVE_ROWS_ACTION,
    isVisible: (env) => CAN_REMOVE_COLUMNS_ROWS("ROW", env),
};
const deleteRow = {
    ...deleteRows,
    isVisible: IS_ONLY_ONE_RANGE,
};
const clearRows = {
    name: DELETE_CONTENT_ROWS_NAME,
    execute: DELETE_CONTENT_ROWS_ACTION,
};
const deleteCols = {
    name: REMOVE_COLUMNS_NAME,
    execute: REMOVE_COLUMNS_ACTION,
    isVisible: (env) => CAN_REMOVE_COLUMNS_ROWS("COL", env),
};
const deleteCol = {
    ...deleteCols,
    isVisible: IS_ONLY_ONE_RANGE,
};
const clearCols = {
    name: DELETE_CONTENT_COLUMNS_NAME,
    execute: DELETE_CONTENT_COLUMNS_ACTION,
};
const deleteCells = {
    name: _t("Delete cells"),
    isVisible: IS_ONLY_ONE_RANGE,
};
const deleteCellShiftUp = {
    name: _t("Delete cell and shift up"),
    execute: (env) => {
        const zone = env.model.getters.getSelectedZone();
        const result = env.model.dispatch("DELETE_CELL", { zone, shiftDimension: "ROW" });
        handlePasteResult(env, result);
    },
};
const deleteCellShiftLeft = {
    name: _t("Delete cell and shift left"),
    execute: (env) => {
        const zone = env.model.getters.getSelectedZone();
        const result = env.model.dispatch("DELETE_CELL", { zone, shiftDimension: "COL" });
        handlePasteResult(env, result);
    },
};
const mergeCells = {
    name: _t("Merge cells"),
    isEnabled: (env) => !cannotMerge(env),
    isActive: (env) => hasMergeInAnySelectedZone(env),
    execute: (env) => toggleMerge(env),
    icon: "o-spreadsheet-Icon.MERGE_CELL",
};
const editTable = {
    name: () => _t("Edit table"),
    execute: (env) => env.openSidePanel("TableSidePanel", {}),
    icon: "o-spreadsheet-Icon.EDIT_TABLE",
};
const deleteTable = {
    name: () => _t("Delete table"),
    execute: DELETE_SELECTED_TABLE,
    icon: "o-spreadsheet-Icon.DELETE_TABLE",
};
function cannotMerge(env) {
    const zones = env.model.getters.getSelectedZones();
    const { sheetId } = env.model.getters.getActivePosition();
    const { xSplit, ySplit } = env.model.getters.getPaneDivisions(sheetId);
    return (zones.every((zone) => getZoneArea(zone) === 1) ||
        doesAnyZoneCrossFrozenPane(zones, xSplit, ySplit) ||
        hasOverlappingZones(zones));
}
function hasMergeInAnySelectedZone(env) {
    if (cannotMerge(env)) {
        return false;
    }
    const sheetId = env.model.getters.getActiveSheetId();
    const zones = env.model.getters.getSelectedZones();
    return zones.some((zone) => {
        return env.model.getters.getMergesInZone(sheetId, zone).length > 0;
    });
}
function toggleMerge(env) {
    if (cannotMerge(env)) {
        return;
    }
    const target = env.model.getters.getSelectedZones();
    const sheetId = env.model.getters.getActiveSheetId();
    if (hasMergeInAnySelectedZone(env)) {
        const mergesToRemove = target.flatMap((zone) => env.model.getters.getMergesInZone(sheetId, zone));
        env.model.dispatch("REMOVE_MERGE", { sheetId, target: mergesToRemove });
    }
    else {
        interactiveAddMerge(env, sheetId, target);
    }
}

const insertRow = {
    name: (env) => {
        const number = getRowsNumber(env);
        return number === 1 ? _t("Insert row") : _t("Insert %s rows", number.toString());
    },
    isVisible: (env) => CAN_INSERT_HEADER(env, "ROW"),
    icon: "o-spreadsheet-Icon.INSERT_ROW",
};
const rowInsertRowBefore = {
    name: (env) => {
        const number = getRowsNumber(env);
        return number === 1 ? _t("Insert row above") : _t("Insert %s rows above", number.toString());
    },
    execute: INSERT_ROWS_BEFORE_ACTION,
    isVisible: (env) => CAN_INSERT_HEADER(env, "ROW"),
    icon: "o-spreadsheet-Icon.INSERT_ROW_BEFORE",
};
const topBarInsertRowsBefore = {
    ...rowInsertRowBefore,
    name: (env) => {
        const number = getRowsNumber(env);
        if (number === 1) {
            return _t("Row above");
        }
        return _t("%s Rows above", number.toString());
    },
};
const cellInsertRowsBefore = {
    ...rowInsertRowBefore,
    name: (env) => {
        const number = getRowsNumber(env);
        if (number === 1) {
            return _t("Insert row");
        }
        return _t("Insert %s rows", number.toString());
    },
    isVisible: IS_ONLY_ONE_RANGE,
    icon: "o-spreadsheet-Icon.INSERT_ROW_BEFORE",
};
const rowInsertRowsAfter = {
    execute: INSERT_ROWS_AFTER_ACTION,
    name: (env) => {
        const number = getRowsNumber(env);
        return number === 1 ? _t("Insert row below") : _t("Insert %s rows below", number.toString());
    },
    isVisible: (env) => CAN_INSERT_HEADER(env, "ROW"),
    icon: "o-spreadsheet-Icon.INSERT_ROW_AFTER",
};
const topBarInsertRowsAfter = {
    ...rowInsertRowsAfter,
    name: (env) => {
        const number = getRowsNumber(env);
        if (number === 1) {
            return _t("Row below");
        }
        return _t("%s Rows below", number.toString());
    },
};
const insertCol = {
    name: (env) => {
        const number = getColumnsNumber(env);
        return number === 1 ? _t("Insert column") : _t("Insert %s columns", number.toString());
    },
    isVisible: (env) => CAN_INSERT_HEADER(env, "COL"),
    icon: "o-spreadsheet-Icon.INSERT_COL",
};
const colInsertColsBefore = {
    name: (env) => {
        const number = getColumnsNumber(env);
        return number === 1
            ? _t("Insert column left")
            : _t("Insert %s columns left", number.toString());
    },
    execute: INSERT_COLUMNS_BEFORE_ACTION,
    isVisible: (env) => CAN_INSERT_HEADER(env, "COL"),
    icon: "o-spreadsheet-Icon.INSERT_COL_BEFORE",
};
const topBarInsertColsBefore = {
    ...colInsertColsBefore,
    name: (env) => {
        const number = getColumnsNumber(env);
        if (number === 1) {
            return _t("Column left");
        }
        return _t("%s Columns left", number.toString());
    },
};
const cellInsertColsBefore = {
    ...colInsertColsBefore,
    name: (env) => {
        const number = getColumnsNumber(env);
        if (number === 1) {
            return _t("Insert column");
        }
        return _t("Insert %s columns", number.toString());
    },
    isVisible: IS_ONLY_ONE_RANGE,
    icon: "o-spreadsheet-Icon.INSERT_COL_BEFORE",
};
const colInsertColsAfter = {
    name: (env) => {
        const number = getColumnsNumber(env);
        return number === 1
            ? _t("Insert column right")
            : _t("Insert %s columns right", number.toString());
    },
    execute: INSERT_COLUMNS_AFTER_ACTION,
    isVisible: (env) => CAN_INSERT_HEADER(env, "COL"),
    icon: "o-spreadsheet-Icon.INSERT_COL_AFTER",
};
const topBarInsertColsAfter = {
    ...colInsertColsAfter,
    name: (env) => {
        const number = getColumnsNumber(env);
        if (number === 1) {
            return _t("Column right");
        }
        return _t("%s Columns right", number.toString());
    },
    execute: INSERT_COLUMNS_AFTER_ACTION,
};
const insertCell = {
    name: _t("Insert cells"),
    isVisible: (env) => IS_ONLY_ONE_RANGE(env) &&
        env.model.getters.getActiveCols().size === 0 &&
        env.model.getters.getActiveRows().size === 0,
    icon: "o-spreadsheet-Icon.INSERT_CELL",
};
const insertCellShiftDown = {
    name: _t("Insert cells and shift down"),
    execute: (env) => {
        const zone = env.model.getters.getSelectedZone();
        const result = env.model.dispatch("INSERT_CELL", { zone, shiftDimension: "ROW" });
        handlePasteResult(env, result);
    },
    isVisible: (env) => env.model.getters.getActiveRows().size === 0 && env.model.getters.getActiveCols().size === 0,
    icon: "o-spreadsheet-Icon.INSERT_CELL_SHIFT_DOWN",
};
const insertCellShiftRight = {
    name: _t("Insert cells and shift right"),
    execute: (env) => {
        const zone = env.model.getters.getSelectedZone();
        const result = env.model.dispatch("INSERT_CELL", { zone, shiftDimension: "COL" });
        handlePasteResult(env, result);
    },
    isVisible: (env) => env.model.getters.getActiveRows().size === 0 && env.model.getters.getActiveCols().size === 0,
    icon: "o-spreadsheet-Icon.INSERT_CELL_SHIFT_RIGHT",
};
const insertChart = {
    name: _t("Chart"),
    execute: CREATE_CHART,
    isEnabled: (env) => !env.isSmall,
    icon: "o-spreadsheet-Icon.INSERT_CHART",
};
const insertCarousel = {
    name: _t("Carousel"),
    execute: CREATE_CAROUSEL,
    isEnabled: (env) => !env.isSmall,
    icon: "o-spreadsheet-Icon.CAROUSEL",
};
const insertPivot = {
    name: _t("Pivot table"),
    execute: CREATE_PIVOT,
    isEnabled: (env) => !env.isSmall,
    icon: "o-spreadsheet-Icon.PIVOT",
};
const insertImage = {
    name: _t("Image"),
    description: "Ctrl+O",
    execute: CREATE_IMAGE,
    isVisible: (env) => env.imageProvider !== undefined,
    isEnabled: (env) => !env.isSmall,
    icon: "o-spreadsheet-Icon.INSERT_IMAGE",
};
const insertTable = {
    name: () => _t("Table"),
    execute: INSERT_TABLE,
    isVisible: (env) => IS_SELECTION_CONTINUOUS(env) && !env.model.getters.getFirstTableInSelection(),
    isEnabled: (env) => !env.isSmall,
    icon: "o-spreadsheet-Icon.PAINT_TABLE",
};
const insertFunction = {
    name: _t("Function"),
    icon: "o-spreadsheet-Icon.FORMULA",
};
const insertFunctionSum = {
    name: _t("SUM"),
    execute: (env) => env.startCellEdition(`=SUM(`),
};
const insertFunctionAverage = {
    name: _t("AVERAGE"),
    execute: (env) => env.startCellEdition(`=AVERAGE(`),
};
const insertFunctionCount = {
    name: _t("COUNT"),
    execute: (env) => env.startCellEdition(`=COUNT(`),
};
const insertFunctionMax = {
    name: _t("MAX"),
    execute: (env) => env.startCellEdition(`=MAX(`),
};
const insertFunctionMin = {
    name: _t("MIN"),
    execute: (env) => env.startCellEdition(`=MIN(`),
};
const categorieFunctionAll = {
    name: _t("All"),
    children: [allFunctionListMenuBuilder],
};
function allFunctionListMenuBuilder() {
    const fnNames = functionRegistry.getKeys().filter((key) => !functionRegistry.get(key).hidden);
    return createFormulaFunctions(fnNames);
}
const categoriesFunctionListMenuBuilder = () => {
    const functions = functionRegistry.content;
    const categories = [
        ...new Set(functionRegistry
            .getAll()
            .filter((fn) => !fn.hidden)
            .map((fn) => fn.category)),
    ].filter(isDefined);
    return categories.sort().map((category, i) => {
        const functionsInCategory = Object.keys(functions).filter((key) => functions[key].category === category && !functions[key].hidden);
        return {
            name: category,
            children: createFormulaFunctions(functionsInCategory),
        };
    });
};
const insertLink = {
    name: _t("Link"),
    execute: INSERT_LINK,
    icon: "o-spreadsheet-Icon.INSERT_LINK",
};
const insertCheckbox = {
    name: _t("Checkbox"),
    execute: (env) => {
        const zones = env.model.getters.getSelectedZones();
        const sheetId = env.model.getters.getActiveSheetId();
        const ranges = zones.map((zone) => env.model.getters.getRangeDataFromZone(sheetId, zone));
        env.model.dispatch("ADD_DATA_VALIDATION_RULE", {
            ranges,
            sheetId,
            rule: {
                id: env.model.uuidGenerator.smallUuid(),
                criterion: {
                    type: "isBoolean",
                    values: [],
                },
            },
        });
    },
    icon: "o-spreadsheet-Icon.INSERT_CHECKBOX",
};
const insertDropdown = {
    name: _t("Dropdown list"),
    execute: (env) => {
        const zones = env.model.getters.getSelectedZones();
        const sheetId = env.model.getters.getActiveSheetId();
        const ranges = zones.map((zone) => env.model.getters.getRangeDataFromZone(sheetId, zone));
        const ruleID = env.model.uuidGenerator.smallUuid();
        env.model.dispatch("ADD_DATA_VALIDATION_RULE", {
            ranges,
            sheetId,
            rule: {
                id: ruleID,
                criterion: {
                    type: "isValueInList",
                    values: [],
                    displayStyle: "chip",
                },
            },
        });
        const rule = env.model.getters.getDataValidationRule(sheetId, ruleID);
        if (!rule) {
            return;
        }
        env.openSidePanel("DataValidationEditor", {
            rule: localizeDataValidationRule(rule, env.model.getters.getLocale()),
            onExit: () => {
                env.replaceSidePanel("DataValidation", "DataValidationEditor");
            },
        });
    },
    isEnabled: (env) => !env.isSmall,
    icon: "o-spreadsheet-Icon.INSERT_DROPDOWN",
};
const insertSheet = {
    name: _t("Insert sheet"),
    execute: (env) => {
        const activeSheetId = env.model.getters.getActiveSheetId();
        const position = env.model.getters.getSheetIds().indexOf(activeSheetId) + 1;
        const sheetId = env.model.uuidGenerator.smallUuid();
        env.model.dispatch("CREATE_SHEET", {
            sheetId,
            position,
            name: env.model.getters.getNextSheetName(),
        });
        env.model.dispatch("ACTIVATE_SHEET", { sheetIdFrom: activeSheetId, sheetIdTo: sheetId });
    },
    icon: "o-spreadsheet-Icon.INSERT_SHEET",
};
function createFormulaFunctions(fnNames) {
    return fnNames.sort().map((fnName, i) => {
        return {
            name: fnName,
            sequence: i * 10,
            execute: (env) => env.startCellEdition(`=${fnName}(`),
        };
    });
}
function getRowsNumber(env) {
    const activeRows = env.model.getters.getActiveRows();
    if (activeRows.size) {
        return activeRows.size;
    }
    else {
        const zone = env.model.getters.getSelectedZones()[0];
        return zone.bottom - zone.top + 1;
    }
}
function getColumnsNumber(env) {
    const activeCols = env.model.getters.getActiveCols();
    if (activeCols.size) {
        return activeCols.size;
    }
    else {
        const zone = env.model.getters.getSelectedZones()[0];
        return zone.right - zone.left + 1;
    }
}

const PREVIOUS_VALUE = "(previous)";
const NEXT_VALUE = "(next)";
function getDomainOfParentRow(pivot, domain) {
    const { colDomain, rowDomain } = domainToColRowDomain(pivot, domain);
    return [...colDomain, ...rowDomain.slice(0, rowDomain.length - 1)];
}
function getDomainOfParentCol(pivot, domain) {
    const { colDomain, rowDomain } = domainToColRowDomain(pivot, domain);
    return [...colDomain.slice(0, colDomain.length - 1), ...rowDomain];
}
/**
 * Split a pivot domain into the part related to the rows of the pivot, and the part related to the columns.
 */
function domainToColRowDomain(pivot, domain) {
    const rowFields = pivot.definition.rows.map((c) => c.nameWithGranularity);
    const rowDomain = domain.filter((node) => rowFields.includes(node.field));
    const columnFields = pivot.definition.columns.map((c) => c.nameWithGranularity);
    const colDomain = domain.filter((node) => columnFields.includes(node.field));
    return { colDomain, rowDomain };
}
function getDimensionDomain(pivot, dimension, domain) {
    return dimension === "column"
        ? domainToColRowDomain(pivot, domain).colDomain
        : domainToColRowDomain(pivot, domain).rowDomain;
}
function getFieldValueInDomain(fieldNameWithGranularity, domain) {
    const node = domain.find((n) => n.field === fieldNameWithGranularity);
    return node?.value;
}
function isDomainIsInPivot(pivot, domain) {
    for (const node of domain) {
        if (pivot.definition.rows.find((row) => row.nameWithGranularity === node.field) === undefined &&
            pivot.definition.columns.find((col) => col.nameWithGranularity === node.field) === undefined) {
            return false;
        }
    }
    const { rowDomain, colDomain } = domainToColRowDomain(pivot, domain);
    return (checkIfDomainInInTree(rowDomain, pivot.getExpandedTableStructure().getRowTree()) &&
        checkIfDomainInInTree(colDomain, pivot.getExpandedTableStructure().getColTree()));
}
function checkIfDomainInInTree(domain, tree) {
    return walkDomainTree(domain, tree) !== undefined;
}
/**
 * Given a tree of the col/rows of a pivot, and a domain related to those col/rows, return the node of the tree
 * corresponding to the domain.
 *
 * @param domain The domain to find in the tree
 * @param tree The tree to search in7
 * @param stopAtField If provided, the search will stop at the field with this name
 */
function walkDomainTree(domain, tree, stopAtField) {
    let currentTreeNode = tree;
    for (const node of domain) {
        const child = currentTreeNode.find((n) => n.value === node.value);
        if (!child) {
            return undefined;
        }
        if (child.field === stopAtField) {
            return currentTreeNode;
        }
        currentTreeNode = child.children;
    }
    return currentTreeNode;
}
/**
 * Get the domain parent of the given domain with the field `parentFieldName` as leaf of the domain.
 *
 * In practice, if the `parentFieldName` is a row in the pivot, the helper will return a domain with the same column
 * domain, and with a row domain all groupBys children to `parentFieldName` removed.
 */
function getFieldParentDomain(pivot, parentFieldName, domain) {
    let { rowDomain, colDomain } = domainToColRowDomain(pivot, domain);
    const dimension = getFieldDimensionType(pivot, parentFieldName);
    if (dimension === "row") {
        const index = rowDomain.findIndex((node) => node.field === parentFieldName);
        if (index === -1) {
            return domain;
        }
        rowDomain = rowDomain.slice(0, index + 1);
    }
    else {
        const index = colDomain.findIndex((node) => node.field === parentFieldName);
        if (index === -1) {
            return domain;
        }
        colDomain = colDomain.slice(0, index + 1);
    }
    return [...rowDomain, ...colDomain];
}
/**
 * Replace in the domain the value of the field `fieldNameWithGranularity` with the given `value`
 */
function replaceFieldValueInDomain(domain, fieldNameWithGranularity, value) {
    domain = deepCopy(domain);
    const node = domain.find((n) => n.field === fieldNameWithGranularity);
    if (!node) {
        return domain;
    }
    node.value = value;
    return domain;
}
function isFieldInDomain(nameWithGranularity, domain) {
    return domain.some((node) => node.field === nameWithGranularity);
}
/**
 * Check if the field is in the rows or columns of the pivot
 */
function getFieldDimensionType(pivot, nameWithGranularity) {
    const rowFields = pivot.definition.rows.map((c) => c.nameWithGranularity);
    if (rowFields.includes(nameWithGranularity)) {
        return "row";
    }
    const columnFields = pivot.definition.columns.map((c) => c.nameWithGranularity);
    if (columnFields.includes(nameWithGranularity)) {
        return "column";
    }
    throw new Error(`Field ${nameWithGranularity} not found in pivot`);
}
/**
 * Replace in the given domain the value of the field `fieldNameWithGranularity` with the previous or next value.
 */
function getPreviousOrNextValueDomain(pivot, domain, fieldNameWithGranularity, direction) {
    const dimension = getFieldDimensionType(pivot, fieldNameWithGranularity);
    const tree = dimension === "row"
        ? pivot.getExpandedTableStructure().getRowTree()
        : pivot.getExpandedTableStructure().getColTree();
    const dimDomain = getDimensionDomain(pivot, dimension, domain);
    const currentTreeNode = walkDomainTree(dimDomain, tree, fieldNameWithGranularity);
    const values = currentTreeNode?.map((n) => n.value) ?? [];
    const value = getFieldValueInDomain(fieldNameWithGranularity, domain);
    if (value === undefined) {
        return undefined;
    }
    const valueIndex = values.indexOf(value);
    if (value === undefined || valueIndex === -1) {
        return undefined;
    }
    const offset = direction === PREVIOUS_VALUE ? -1 : 1;
    const newIndex = clip(valueIndex + offset, 0, values.length - 1);
    return replaceFieldValueInDomain(domain, fieldNameWithGranularity, values[newIndex]);
}
function domainToString(domain) {
    return domain ? domain.map(domainNodeToString).join(", ") : "";
}
function domainNodeToString(domainNode) {
    return domainNode ? `${domainNode.field}=${domainNode.value}` : "";
}
/**
 *
 * For the ranking, the pivot cell values of a column (or row) at the same depth are grouped together before being sorted
 * and ranked.
 *
 * The grouping of a pivot cell is done with both the value of the domain nodes that are parent of the field
 * `fieldNameWithGranularity` and the value of the last node of the domain of the pivot cell, if it's not the field
 * `fieldNameWithGranularity`.
 *
 * For example, let's take a pivot grouped by (Date:year, Stage, User, Product), where we want to rank by "Stage" field.
 * The domain nodes parents of the "Stage" are [Date:year]. The pivot cell with domain:
 * - [Date:year=2021] is not ranked because it does not contain the "Stage" field
 * - [Date:year=2021, Stage=Lead] is grouped with the cells [Date:year=2021, Stage=*, User=None, Product=None],
 *      and then ranked within the group
 * - [Date:year=2021, Stage=Lead, User=Bob] is grouped with the cells [Date:year=2021, Stage=*, User=Bob, Product=None],
 *      and then ranked within the group
 * - [Date:year=2021, Stage=Lead, User=Bob, Product=Table] is grouped with the cells [Date:year=2021, Stage=*, User=*, Product=Table],
 *      and then ranked within the group
 *
 * If we rank the pivot on "User" instead, the parent domain becomes [Date:year, Sage] .The cell with domain:
 * - [Date:year=2021] is not ranked because it does not contain the "Stage" field
 * - [Date:year=2021, Stage=Lead] is not ranked because it does not contain the "User" field
 * - [Date:year=2021, Stage=Lead, User=Bob] is grouped with the cells [Date:year=2021, Stage=Lead, User=Bob, Product=None],
 *      and then ranked within the group
 * - [Date:year=2021, Stage=Lead, User=Bob, Product=Table] is grouped with the cells with [Date:year=2021, Stage=Lead, User=*, Product=Table],
 *     and then ranked within the group
 *
 */
function getRankingDomainKey(domain, fieldNameWithGranularity) {
    const index = domain.findIndex((node) => node.field === fieldNameWithGranularity);
    if (index === -1) {
        return "";
    }
    const parent = domain.slice(0, index);
    const lastNode = domain.at(-1);
    return domainToString(lastNode.field === fieldNameWithGranularity ? parent : [...parent, lastNode]);
}
/**
 * The running total domain is the domain without the field `fieldNameWithGranularity`, ie. we do the running total of
 * all the pivot cells of the column that have any value for the field `fieldNameWithGranularity` and the same value for
 * the other fields.
 */
function getRunningTotalDomainKey(domain, fieldNameWithGranularity) {
    const index = domain.findIndex((node) => node.field === fieldNameWithGranularity);
    if (index === -1) {
        return "";
    }
    return domainToString([...domain.slice(0, index), ...domain.slice(index + 1)]);
}
function sortPivotTree(tree, baseDomain, sortFn) {
    const sortedTree = [...tree];
    const domain = [...baseDomain];
    sortedTree.sort((node1, node2) => sortFn([...domain, node1], [...domain, node2]));
    for (const node of tree) {
        const children = sortPivotTree(node.children, [...domain, node], sortFn);
        node.children = children;
    }
    return sortedTree;
}
function isParentDomain(domain, parentDomain) {
    return (domain.length > parentDomain.length &&
        parentDomain.every((node, i) => deepEquals(node, domain[i])));
}

/**
 * Represent a pivot runtime definition. A pivot runtime definition is a pivot
 * definition that has been enriched to include the display name of its attributes
 * (measures, columns, rows).
 */
class PivotRuntimeDefinition {
    measures;
    columns;
    rows;
    sortedColumn;
    collapsedDomains;
    customFields;
    constructor(definition, fields) {
        this.columns = definition.columns.map((dimension) => this.createPivotDimension(fields, dimension));
        this.rows = definition.rows.map((dimension) => this.createPivotDimension(fields, dimension));
        this.measures = definition.measures.map((measure) => this.createMeasure(fields, measure));
        this.sortedColumn = definition.sortedColumn;
        this.collapsedDomains = definition.collapsedDomains;
        this.customFields = definition.customFields;
    }
    getDimension(nameWithGranularity) {
        const dimension = this.columns.find((d) => d.nameWithGranularity === nameWithGranularity) ||
            this.rows.find((d) => d.nameWithGranularity === nameWithGranularity);
        if (!dimension) {
            throw new EvaluationError(_t("Dimension %s does not exist", nameWithGranularity));
        }
        return dimension;
    }
    getMeasure(id) {
        const measure = this.measures.find((measure) => measure.id === id);
        if (!measure) {
            throw new EvaluationError(_t("Field %s is not a measure", id));
        }
        return measure;
    }
    get invalidAggregatorsForCustomField() {
        return [];
    }
    createMeasure(fields, measure) {
        const fieldName = measure.fieldName;
        const field = fieldName === "__count"
            ? { name: "__count", string: _t("Count"), type: "integer", aggregator: "sum" }
            : fields[fieldName];
        const aggregator = measure.aggregator;
        let isValid = !!(field || measure.computedBy);
        for (const dimension of [...this.rows, ...this.columns]) {
            const field = fields[dimension.fieldName];
            if (field?.isCustomField && this.invalidAggregatorsForCustomField.includes(aggregator)) {
                isValid = false;
                break;
            }
        }
        return {
            /**
             * Get the id of the measure, as it is stored in the pivot formula
             */
            id: measure.id,
            /**
             * Display name of the measure
             * e.g. "__count" -> "Count", "amount_total" -> "Total Amount"
             */
            get displayName() {
                return measure.userDefinedName ?? field?.string ?? measure.fieldName;
            },
            userDefinedName: measure.userDefinedName,
            /**
             * Get the name of the field of the measure
             */
            fieldName,
            /**
             * Get the aggregator of the measure
             */
            aggregator,
            /**
             * Get the type of the measure field
             * e.g. "stage_id" -> "many2one", "create_date:month" -> "date"
             */
            type: fieldName === "__count" ? "integer" : field?.type ?? "integer",
            isValid,
            isHidden: measure.isHidden,
            format: measure.format,
            computedBy: measure.computedBy,
            display: measure.display,
        };
    }
    createPivotDimension(fields, dimension) {
        const field = fields[dimension.fieldName];
        const type = field?.type ?? "integer";
        const granularity = field && isDateOrDatetimeField(field) ? dimension.granularity : undefined;
        return {
            /**
             * Get the display name of the dimension
             * e.g. "stage_id" -> "Stage", "create_date:month" -> "Create Date"
             */
            displayName: field?.string ?? dimension.fieldName,
            /**
             * Get the name of the dimension, as it is stored in the pivot formula
             * e.g. "stage_id", "create_date:month"
             */
            nameWithGranularity: dimension.fieldName + (granularity ? `:${granularity}` : ""),
            /**
             * Get the name of the field of the dimension
             * e.g. "stage_id" -> "stage_id", "create_date:month" -> "create_date"
             */
            fieldName: dimension.fieldName,
            /**
             * Get the aggregate operator of the dimension
             * e.g. "stage_id" -> undefined, "create_date:month" -> "month"
             */
            granularity,
            /**
             * Get the type of the field of the dimension
             * e.g. "stage_id" -> "many2one", "create_date:month" -> "date"
             */
            type: field?.isCustomField ? "custom" : type,
            order: dimension.order,
            isValid: !!field,
            isCustomField: !!field?.isCustomField,
            customGroups: field?.customGroups,
            parentField: field?.parentField,
        };
    }
}

class SpreadsheetPivotRuntimeDefinition extends PivotRuntimeDefinition {
    range;
    constructor(definition, fields, getters) {
        super(definition, fields);
        if (definition.dataSet) {
            const { sheetId, zone } = definition.dataSet;
            this.range = getters.getRangeFromZone(sheetId, zone);
        }
    }
}

/**
 * Class used to ease the construction of a pivot table.
 * Let's consider the following example, with:
 * - columns groupBy: [sales_team, create_date]
 * - rows groupBy: [continent, city]
 * - measures: [revenues]
 * _____________________________________________________________________________________|   ----|
 * |                |   Sale Team 1             |  Sale Team 2            |             |       |
 * |                |___________________________|_________________________|_____________|       |
 * |                |   May 2020   | June 2020  |  May 2020  | June 2020  |   Total     |       |<---- `cols`
 * |                |______________|____________|____________|____________|_____________|       |   ----|
 * |                |   Revenues   |  Revenues  |  Revenues  |  Revenues  |   Revenues  |       |       |<--- `measureRow`
 * |________________|______________|____________|____________|____________|_____________|   ----|   ----|
 * |Europe          |     25       |     35     |     40     |     30     |     65      |   ----|
 * |    Brussels    |      0       |     15     |     30     |     30     |     30      |       |
 * |    Paris       |     25       |     20     |     10     |     0      |     35      |       |
 * |North America   |     60       |     75     |            |            |     60      |       |<---- `body`
 * |    Washington  |     60       |     75     |            |            |     60      |       |
 * |Total           |     85       |     110    |     40     |     30     |     125     |       |
 * |________________|______________|____________|____________|____________|_____________|   ----|
 *
 * |                |
 * |----------------|
 *         |
 *         |
 *       `rows`
 *
 * `rows` is an array of cells, each cells contains the indent level, the fields used for the group by and the values for theses fields.
 * For example:
 *   `Europe`: { indent: 1, fields: ["continent"], values: ["id_of_Europe"]}
 *   `Brussels`: { indent: 2, fields: ["continent", "city"], values: ["id_of_Europe", "id_of_Brussels"]}
 *   `Total`: { indent: 0, fields: [], values: []}
 *
 * `columns` is an double array, first by row and then by cell. So, in this example, it looks like:
 *   [[row1], [row2], [measureRow]]
 *   Each cell of a column's row contains the width (span) of the cells, the fields used for the group by and the values for theses fields.
 * For example:
 *   `Sale Team 1`: { width: 2, fields: ["sales_team"], values: ["id_of_SaleTeam1"]}
 *   `May 2020` (the one under Sale Team 2): { width: 1, fields: ["sales_team", "create_date"], values: ["id_of_SaleTeam2", "May 2020"]}
 *   `Revenues` (the one under Total): { width: 1, fields: ["measure"], values: ["revenues"]}
 *
 */
class SpreadsheetPivotTable {
    columns;
    rows;
    measures;
    fieldsType;
    maxIndent;
    pivotCells = {};
    rowTree;
    colTree;
    isSorted = false;
    constructor(columns, rows, measures, fieldsType, collapsedDomains = { COL: [], ROW: [] }) {
        this.measures = measures;
        this.fieldsType = fieldsType;
        if (collapsedDomains.COL.length) {
            columns = this.removeCollapsedColumns(columns, measures, collapsedDomains.COL);
        }
        this.columns = columns.map((cols) => {
            // offset in the pivot table
            // starts at 1 because the first column is the row title
            let offset = 1;
            return cols.map((col) => {
                col = { ...col, offset };
                offset += col.width;
                return col;
            });
        });
        this.rows = rows.filter((row) => !this.isParentCollapsed(collapsedDomains.ROW, row));
        this.maxIndent = Math.max(...this.rows.map((row) => row.indent));
        this.rowTree = lazy(() => this.buildRowsTree());
        this.colTree = lazy(() => this.buildColumnsTree());
    }
    removeCollapsedColumns(columns, measures, collapsedDomains) {
        const replaceCollapsedChildrenWithSubTotalColumns = (parentCol, depth) => {
            const parentDomain = this.getDomain(parentCol);
            const cols = columns[depth];
            const startIndex = cols.findIndex((col) => isParentDomain(this.getDomain(col), parentDomain));
            const endIndex = cols.findLastIndex((col) => isParentDomain(this.getDomain(col), parentDomain));
            const isLeaf = depth === columns.length - 1;
            const newColumns = measures.map((measure) => {
                const fields = isLeaf ? [...parentCol.fields, "measure"] : [];
                const values = isLeaf ? [...parentCol.values, measure] : [];
                return { fields, values, width: 1, offset: 0, collapsedHeader: !isLeaf };
            });
            cols.splice(startIndex, endIndex - startIndex + 1, ...newColumns);
        };
        return columns.map((cols, i) => {
            for (const col of cols) {
                if (i >= columns.length - 2) {
                    return cols;
                }
                const domain = this.getDomain(col);
                if (!collapsedDomains.some((collapsedDomain) => deepEquals(domain, collapsedDomain))) {
                    continue;
                }
                col.width = measures.length;
                for (let depth = i + 1; depth < columns.length; depth++) {
                    replaceCollapsedChildrenWithSubTotalColumns(col, depth);
                }
            }
            return cols;
        });
    }
    isParentCollapsed(collapsedDomains, dim) {
        const domain = this.getDomain(dim);
        return collapsedDomains.some((collapsedDomain) => isParentDomain(domain, collapsedDomain));
    }
    /**
     * Get the number of columns leafs (i.e. the number of the last row of columns)
     */
    getNumberOfDataColumns() {
        return this.columns.at(-1)?.length || 0;
    }
    getSkippedRows(visibilityOptions) {
        const skippedRows = new Set();
        if (!visibilityOptions.displayColumnHeaders) {
            for (let i = 0; i < this.columns.length - 1; i++) {
                skippedRows.add(i);
            }
        }
        if (!visibilityOptions.displayMeasuresRow) {
            skippedRows.add(this.columns.length - 1);
        }
        return skippedRows;
    }
    getPivotCells(visibilityOptions = {
        displayColumnHeaders: true,
        displayTotals: true,
        displayMeasuresRow: true,
    }) {
        const key = JSON.stringify(visibilityOptions);
        if (!this.pivotCells[key]) {
            const { displayTotals } = visibilityOptions;
            const numberOfDataRows = this.rows.length;
            const numberOfDataColumns = this.getNumberOfDataColumns();
            let pivotHeight = this.columns.length + numberOfDataRows;
            let pivotWidth = 1 /*(row headers)*/ + numberOfDataColumns;
            if (!displayTotals && numberOfDataRows !== 1) {
                pivotHeight -= 1;
            }
            if (!displayTotals && numberOfDataColumns !== this.measures.length) {
                pivotWidth -= this.measures.length;
            }
            const domainArray = [];
            const skippedRows = this.getSkippedRows(visibilityOptions);
            for (let col = 0; col < pivotWidth; col++) {
                domainArray.push([]);
                for (let row = 0; row < pivotHeight; row++) {
                    if (skippedRows.has(row)) {
                        continue;
                    }
                    domainArray[col].push(this.getPivotCell(col, row, displayTotals));
                }
            }
            this.pivotCells[key] = domainArray;
        }
        return this.pivotCells[key];
    }
    getRowTree() {
        return this.rowTree();
    }
    getColTree() {
        return this.colTree();
    }
    isTotalRow(index) {
        return this.rows[index].indent !== this.maxIndent;
    }
    getPivotCell(col, row, includeTotal = true) {
        const colHeadersHeight = this.columns.length;
        if (col > 0 && row === colHeadersHeight - 1) {
            const domain = this.getColHeaderDomain(col, row);
            if (!domain) {
                return EMPTY_PIVOT_CELL;
            }
            const measure = domain.at(-1)?.value?.toString() || "";
            return { type: "MEASURE_HEADER", domain: domain.slice(0, -1), measure };
        }
        else if (row <= colHeadersHeight - 1) {
            const domain = this.getColHeaderDomain(col, row);
            return domain ? { type: "HEADER", domain, dimension: "COL" } : EMPTY_PIVOT_CELL;
        }
        else if (col === 0) {
            const rowIndex = row - colHeadersHeight;
            const domain = this.getDomain(this.rows[rowIndex]);
            return { type: "HEADER", domain, dimension: "ROW" };
        }
        else {
            const rowIndex = row - colHeadersHeight;
            if (!includeTotal && this.isTotalRow(rowIndex)) {
                return EMPTY_PIVOT_CELL;
            }
            const domain = [...this.getDomain(this.rows[rowIndex]), ...this.getColDomain(col)];
            const measure = this.getColMeasure(col);
            return { type: "VALUE", domain, measure };
        }
    }
    getColHeaderDomain(col, row) {
        if (col === 0) {
            return undefined;
        }
        const pivotCol = this.columns[row].find((pivotCol) => pivotCol.offset === col);
        if (!pivotCol || pivotCol.collapsedHeader) {
            return undefined;
        }
        return this.getDomain(pivotCol);
    }
    getDomain(dim) {
        return dim.fields.map((fieldWithGranularity, i) => {
            if (fieldWithGranularity === "measure") {
                return {
                    type: "char",
                    field: fieldWithGranularity,
                    value: toNormalizedPivotValue({ displayName: "measure", type: "char" }, dim.values[i]),
                };
            }
            else {
                const { fieldName, granularity } = parseDimension(fieldWithGranularity);
                const type = this.fieldsType[fieldName] || "char";
                return {
                    type,
                    field: fieldWithGranularity,
                    value: toNormalizedPivotValue({ displayName: fieldName, type, granularity }, dim.values[i]),
                };
            }
        });
    }
    getColDomain(col) {
        const domain = this.getColHeaderDomain(col, this.columns.length - 1);
        return domain ? domain.slice(0, -1) : []; // slice: remove measure and value
    }
    getColMeasure(col) {
        const domain = this.getColHeaderDomain(col, this.columns.length - 1);
        const measure = domain?.at(-1)?.value;
        if (measure === undefined || measure === null) {
            throw new Error("Measure is missing");
        }
        return measure.toString();
    }
    buildRowsTree() {
        const tree = [];
        let depth = 0;
        const treesAtDepth = {};
        treesAtDepth[0] = tree;
        for (const row of this.rows) {
            if (row.fields.length === 0 || row.values.length === 0) {
                return tree;
            }
            const rowDepth = row.fields.length - 1;
            const fieldWithGranularity = row.fields[rowDepth];
            const { fieldName, granularity } = parseDimension(fieldWithGranularity);
            const type = this.fieldsType[fieldName] ?? "char";
            const value = toNormalizedPivotValue({ displayName: fieldName, type, granularity }, row.values[rowDepth]);
            if (rowDepth > depth) {
                depth = rowDepth;
                treesAtDepth[depth] = [];
                const parentNode = treesAtDepth[depth - 1].at(-1);
                if (parentNode) {
                    parentNode.children = treesAtDepth[depth];
                }
            }
            depth = rowDepth;
            const node = {
                value,
                field: row.fields[rowDepth],
                children: [],
                type: this.fieldsType[fieldName] || "char",
                width: 0, // not used
            };
            treesAtDepth[depth].push(node);
        }
        return tree;
    }
    buildColumnsTree() {
        const tree = [];
        const columns = this.columns.at(-2) || [];
        const treesAtDepth = {};
        treesAtDepth[0] = tree;
        for (const leaf of columns) {
            for (let depth = 0; depth < leaf.fields.length; depth++) {
                const fieldWithGranularity = leaf.fields[depth];
                const { fieldName, granularity } = parseDimension(fieldWithGranularity);
                const type = this.fieldsType[fieldName] ?? "char";
                const value = toNormalizedPivotValue({ displayName: fieldName, type, granularity }, leaf.values[depth]);
                const node = {
                    value,
                    field: leaf.fields[depth],
                    children: [],
                    width: leaf.width,
                    type: this.fieldsType[fieldName] || "char",
                };
                if (treesAtDepth[depth]?.at(-1)?.value !== value) {
                    treesAtDepth[depth + 1] = [];
                    node.children = treesAtDepth[depth + 1];
                    treesAtDepth[depth].push(node);
                }
            }
        }
        return tree;
    }
    export() {
        return {
            cols: this.columns,
            rows: this.rows,
            measures: this.measures,
            fieldsType: this.fieldsType,
        };
    }
    sort(measure, sortedColumn, getValue) {
        if (this.isSorted) {
            return;
        }
        const getSortValue = (measure, domain) => {
            const rawValue = getValue(measure, domain).value;
            return typeof rawValue === "number" ? rawValue : -Infinity;
        };
        const sortColDomain = sortedColumn.domain;
        const sortFn = (rowDomain1, rowDomain2) => {
            const value1 = getSortValue(measure, [...rowDomain1, ...sortColDomain]);
            const value2 = getSortValue(measure, [...rowDomain2, ...sortColDomain]);
            return sortedColumn.order === "asc" ? value1 - value2 : value2 - value1;
        };
        const sortedRowTree = sortPivotTree(this.rowTree(), [], sortFn);
        this.rowTree = lazy(sortedRowTree);
        this.rows = [...this.rowTreeToRows(sortedRowTree), this.rows[this.rows.length - 1]];
        this.isSorted = true;
    }
    rowTreeToRows(tree, parentRow) {
        return tree.flatMap((node) => {
            const row = {
                indent: parentRow ? parentRow.indent + 1 : 1,
                fields: [...(parentRow?.fields || []), node.field],
                values: [...(parentRow?.values || []), node.value],
            };
            return [row, ...this.rowTreeToRows(node.children, row)];
        });
    }
    get numberOfCells() {
        return this.rows.length * this.getNumberOfDataColumns();
    }
}
const EMPTY_PIVOT_CELL = { type: "EMPTY" };

/**
 * This function converts a list of data entry into a spreadsheet pivot table.
 */
function dataEntriesToSpreadsheetPivotTable(dataEntries, definition, mode) {
    const measureIds = definition.measures.filter((measure) => !measure.isHidden).map((m) => m.id);
    const columnsTree = dataEntriesToColumnsTree(dataEntries, definition.columns, 0);
    computeWidthOfColumnsNodes(columnsTree, measureIds.length);
    const cols = columnsTreeToColumns(columnsTree, definition);
    const rows = dataEntriesToRows(dataEntries, 0, definition.rows, [], []);
    // Add the total row
    rows.push({
        fields: [],
        values: [],
        indent: 0,
    });
    const fieldsType = {};
    for (const columns of definition.columns) {
        fieldsType[columns.fieldName] = columns.type;
    }
    for (const row of definition.rows) {
        fieldsType[row.fieldName] = row.type;
    }
    const collapsedDomains = mode === "collapsed" ? definition.collapsedDomains : undefined;
    return new SpreadsheetPivotTable(cols, rows, measureIds, fieldsType, collapsedDomains);
}
// -----------------------------------------------------------------------------
// ROWS
// -----------------------------------------------------------------------------
/**
 * Create the rows from the data entries. This function is called recursively
 * for each level of rows dimensions.
 */
function dataEntriesToRows(dataEntries, index, rows, fields, values) {
    if (index >= rows.length) {
        return [];
    }
    const row = rows[index];
    const rowName = row.nameWithGranularity;
    const groups = groupPivotDataEntriesBy(dataEntries, row);
    const orderedKeys = orderDataEntriesKeys(groups, row);
    const pivotTableRows = [];
    const _fields = fields.concat(rowName);
    for (const value of orderedKeys) {
        const _values = values.concat(value);
        pivotTableRows.push({
            fields: _fields,
            values: _values,
            indent: index + 1,
        });
        const record = groups[value];
        if (record) {
            pivotTableRows.push(...dataEntriesToRows(record, index + 1, rows, _fields, _values));
        }
    }
    return pivotTableRows;
}
// -----------------------------------------------------------------------------
// COLUMNS
// -----------------------------------------------------------------------------
/**
 * Create the columns tree from data entries.
 */
function dataEntriesToColumnsTree(dataEntries, columns, index) {
    if (index >= columns.length) {
        return [];
    }
    const column = columns[index];
    const colName = columns[index].nameWithGranularity;
    const groups = groupPivotDataEntriesBy(dataEntries, column);
    const orderedKeys = orderDataEntriesKeys(groups, columns[index]);
    return orderedKeys.map((key) => {
        return {
            value: groups[key]?.[0]?.[column.nameWithGranularity]?.value ?? null,
            field: colName,
            children: dataEntriesToColumnsTree(groups[key] || [], columns, index + 1),
            type: column.type,
            width: 0,
        };
    });
}
/**
 * Compute the width of each node in the column tree.
 * The width of a node is the sum of the width of its children.
 * For leaf nodes, the width is the number of measures.
 */
function computeWidthOfColumnsNodes(tree, measureCount) {
    for (const key in tree) {
        const node = tree[key];
        if (node.children.length === 0) {
            node.width = measureCount;
        }
        else {
            computeWidthOfColumnsNodes(node.children, measureCount);
            node.width = node.children.reduce((acc, child) => acc + child.width, 0);
        }
    }
}
/**
 * Convert the columns tree to the columns
 */
function columnsTreeToColumns(mainTree, definition) {
    const columnNames = definition.columns.map((col) => col.nameWithGranularity);
    const height = columnNames.length;
    const measures = definition.measures.filter((measure) => !measure.isHidden);
    const measureCount = measures.length;
    const headers = new Array(height).fill(0).map(() => []);
    function generateTreeHeaders(tree, rowIndex, val) {
        const row = headers[rowIndex];
        for (const node of tree) {
            const localVal = val.concat([node.value]);
            const cell = {
                fields: columnNames.slice(0, rowIndex + 1),
                values: localVal,
                width: node.width,
                offset: 0,
            };
            row.push(cell);
            if (rowIndex <= height - 1) {
                generateTreeHeaders(node.children, rowIndex + 1, localVal);
            }
        }
    }
    generateTreeHeaders(mainTree, 0, []);
    const hasColGroupBys = columnNames.length > 0;
    // 2) generate measures row
    const measureRow = [];
    if (hasColGroupBys) {
        headers[headers.length - 1].forEach((cell) => {
            measures.forEach((measure) => {
                const measureCell = {
                    fields: [...cell.fields, "measure"],
                    values: [...cell.values, measure.id],
                    width: 1,
                    offset: 0,
                };
                measureRow.push(measureCell);
            });
        });
    }
    // Add the totals of the measures
    measures.forEach((measure) => {
        const measureCell = {
            fields: ["measure"],
            values: [measure.id],
            width: 1,
            offset: 0,
        };
        measureRow.push(measureCell);
    });
    headers.push(measureRow);
    // 3) Add the total cell
    if (headers.length === 1) {
        headers.unshift([]); // Will add the total there
    }
    headers[headers.length - 2].push({
        fields: [],
        values: [],
        width: measureCount,
        offset: 0,
    });
    return headers;
}
// -----------------------------------------------------------------------------
// HELPERS
// -----------------------------------------------------------------------------
/**
 * Group the dataEntries based on the given dimension
 */
function groupPivotDataEntriesBy(dataEntries, dimension) {
    return Object.groupBy(dataEntries, keySelector(dimension));
}
/**
 * Function used to identify the key that should be used to group dataEntries
 */
function keySelector(dimension) {
    const name = dimension.nameWithGranularity;
    return (item) => {
        return `${item[name]?.value ?? null}`;
    };
}
/**
 * Order the keys of the given data entries, based on the given dimension
 */
function orderDataEntriesKeys(groups, dimension) {
    const order = dimension.order;
    const othersGroup = dimension.customGroups?.find((group) => group.isOtherGroup);
    if (!order && !othersGroup) {
        return Object.keys(groups);
    }
    return Object.keys(groups).sort((a, b) => {
        if (othersGroup && a === othersGroup.name) {
            return 1;
        }
        if (othersGroup && b === othersGroup.name) {
            return -1;
        }
        return order ? compareDimensionValues(dimension, a, b) : 0;
    });
}
/**
 * Function used to compare two values, based on the type of the given dimension.
 * Used to order two values
 */
function compareDimensionValues(dimension, a, b) {
    if (a === "null") {
        return dimension.order === "asc" ? 1 : -1;
    }
    if (b === "null") {
        return dimension.order === "asc" ? -1 : 1;
    }
    if (dimension.type === "integer" || dimension.type === "datetime") {
        return dimension.order === "asc"
            ? toNumber(a, DEFAULT_LOCALE) - toNumber(b, DEFAULT_LOCALE)
            : toNumber(b, DEFAULT_LOCALE) - toNumber(a, DEFAULT_LOCALE);
    }
    return dimension.order === "asc" ? a.localeCompare(b) : b.localeCompare(a);
}

const NULL_SYMBOL = Symbol("NULL");
function createDate(dimension, value, locale) {
    const granularity = dimension.granularity || "month";
    if (!(granularity in MAP_VALUE_DIMENSION_DATE)) {
        throw new Error(`Unknown date granularity: ${granularity}`);
    }
    const keyInMap = typeof value === "number" || typeof value === "string" ? value : NULL_SYMBOL;
    if (!MAP_VALUE_DIMENSION_DATE[granularity].set.has(value)) {
        MAP_VALUE_DIMENSION_DATE[granularity].set.add(value);
        let number = null;
        if (typeof value === "number" || typeof value === "string") {
            const date = toJsDate(value, locale);
            switch (granularity) {
                case "year":
                    number = date.getFullYear();
                    break;
                case "quarter_number":
                    number = Math.floor(date.getMonth() / 3) + 1;
                    break;
                case "month_number":
                    number = date.getMonth() + 1;
                    break;
                case "month":
                    number = Math.floor(toNumber(value, locale));
                    break;
                case "iso_week_number":
                    number = date.getIsoWeek();
                    break;
                case "day_of_month":
                    number = date.getDate();
                    break;
                case "day":
                    number = Math.floor(toNumber(value, locale));
                    break;
                case "day_of_week":
                    /**
                     * getDay() returns the day of the week in the range 0-6, with 0
                     * being Sunday. We need to normalize this to the range 1-7, with 1
                     * being the first day of the week depending on the locale.
                     * Normalized value: fr_FR: 1: Monday, 7: Sunday   (weekStart = 1)
                     *                   en_US: 1: Sunday, 7: Saturday (weekStart = 7)
                     */
                    number = ((date.getDay() + 7 - locale.weekStart) % 7) + 1;
                    break;
                case "hour_number":
                    number = date.getHours();
                    break;
                case "minute_number":
                    number = date.getMinutes();
                    break;
                case "second_number":
                    number = date.getSeconds();
                    break;
            }
        }
        MAP_VALUE_DIMENSION_DATE[granularity].values[keyInMap] = toNormalizedPivotValue(dimension, number);
    }
    return MAP_VALUE_DIMENSION_DATE[granularity].values[keyInMap];
}
/**
 * This map is used to cache the different values of a pivot date value
 * 43_831 -> 01/01/2012
 * Example: {
 *   year: {
 *     set: { 43_831 },
 *     values: { '43_831': 2012 }
 *   },
 *   quarter_number: {
 *     set: { 43_831 },
 *     values: { '43_831': 1 }
 *   },
 *   month_number: {
 *     set: { 43_831 },
 *     values: { '43_831': 0 }
 *   },
 *   iso_week_number: {
 *     set: { 43_831 },
 *     values: { '43_831': 1 }
 *   },
 *   day_of_month: {
 *     set: { 43_831 },
 *     values: { '43_831': 1 }
 *   },
 *   day: {
 *     set: { 43_831 },
 *     values: { '43_831': 43_831 }
 *   }
 *   day_of_week: {
 *     set: { 45_387 },
 *     values: { '45_387': 6 } (in locale with startWeek = 7)
 *   }
 *   hour_number: {
 *     set: { 45_387.13 },
 *     values: { '45_387.13': 3 }
 *   }
 *   minute_number: {
 *     set: { 45_387.13 },
 *     values: { '45_387.13': 7 }
 *   }
 *   second_number: {
 *     set: { 45_387.13 },
 *     values: { '45_387.13': 12 }
 *   }
 * }
 */
const MAP_VALUE_DIMENSION_DATE = {
    year: {
        set: new Set(),
        values: {},
    },
    quarter_number: {
        set: new Set(),
        values: {},
    },
    month_number: {
        set: new Set(),
        values: {},
    },
    month: {
        set: new Set(),
        values: {},
    },
    iso_week_number: {
        set: new Set(),
        values: {},
    },
    day_of_month: {
        set: new Set(),
        values: {},
    },
    day: {
        set: new Set(),
        values: {},
    },
    day_of_week: {
        set: new Set(),
        values: {},
    },
    hour_number: {
        set: new Set(),
        values: {},
    },
    minute_number: {
        set: new Set(),
        values: {},
    },
    second_number: {
        set: new Set(),
        values: {},
    },
};
/**
 * Reset the cache of the pivot date values.
 */
function resetMapValueDimensionDate() {
    for (const key in MAP_VALUE_DIMENSION_DATE) {
        MAP_VALUE_DIMENSION_DATE[key].set.clear();
        MAP_VALUE_DIMENSION_DATE[key].values = {};
    }
}

var ReloadType;
(function (ReloadType) {
    ReloadType[ReloadType["NONE"] = 0] = "NONE";
    ReloadType[ReloadType["TABLE"] = 1] = "TABLE";
    ReloadType[ReloadType["DATA"] = 2] = "DATA";
    ReloadType[ReloadType["DEFINITION"] = 3] = "DEFINITION";
    ReloadType[ReloadType["ALL"] = 4] = "ALL";
})(ReloadType || (ReloadType = {}));
/**
 * This class represents a pivot table that is created from a range of cells.
 * It will extract the fields from the first row of the range and the data from
 * the rest of the rows.
 */
class SpreadsheetPivot {
    type = "SPREADSHEET";
    getters;
    _definition;
    coreDefinition;
    metaData = { fields: {}, fieldKeys: [] };
    /**
     * This array contains the data entries of the pivot. Each entry is an object
     * that contains the values of the fields for a row.
     */
    dataEntries = [];
    /**
     * This object contains the pivot table structure. It is created from the
     * data entries and the pivot definition.
     */
    collapsedTable;
    expandedTable;
    /**
     * This error is set when the range is invalid. It is used to show an error
     * message to the user.
     */
    invalidRangeError;
    /**
     * This flag is used to know when the pivot needs to be reloaded. It's only
     * used in the evaluation process. At the end of each cycle, the flag is set
     * to true so the pivot is reloaded in the next cycle.
     */
    needsReevaluation = true;
    constructor(custom, params) {
        this.getters = params.getters;
        this.coreDefinition = params.definition;
    }
    init(params = {}) {
        if (!this._definition || params.reload) {
            this.reload(ReloadType.ALL);
            this.needsReevaluation = false;
        }
    }
    reload(type) {
        if (type === ReloadType.ALL) {
            this.metaData = this.loadMetaData();
        }
        if (type >= ReloadType.DEFINITION) {
            this._definition = this.loadRuntimeDefinition();
        }
        if (type >= ReloadType.DATA) {
            this.dataEntries = this.loadData();
        }
        if (type >= ReloadType.TABLE) {
            this.collapsedTable = undefined;
            this.expandedTable = undefined;
        }
    }
    onDefinitionChange(nextDefinition) {
        const actualDefinition = this.coreDefinition;
        this.coreDefinition = nextDefinition;
        if (this._definition) {
            const reloadType = Math.max(this.computeShouldReload(actualDefinition, nextDefinition), ReloadType.NONE);
            this.reload(reloadType);
        }
    }
    computeShouldReload(actualDefinition, nextDefinition) {
        if (deepEquals(actualDefinition.dataSet, nextDefinition.dataSet)) {
            return ReloadType.DEFINITION;
        }
        return ReloadType.ALL;
    }
    get isInvalidRange() {
        return !!this.invalidRangeError;
    }
    get invalidRangeMessage() {
        return this.invalidRangeError?.message;
    }
    get definition() {
        if (!this._definition) {
            this.init();
        }
        if (!this._definition) {
            throw new Error("Pivot definition should be defined at this point.");
        }
        return this._definition;
    }
    isValid() {
        if (this.invalidRangeError || !this.definition) {
            return false;
        }
        for (const measure of this.definition.measures) {
            if (!measure.isValid) {
                return false;
            }
        }
        for (const column of this.definition.columns) {
            if (!column.isValid) {
                return false;
            }
        }
        for (const row of this.definition.rows) {
            if (!row.isValid) {
                return false;
            }
        }
        return true;
    }
    assertIsValid({ throwOnError }) {
        if (!this.isValid()) {
            if (throwOnError) {
                if (this.invalidRangeError) {
                    throw this.invalidRangeError;
                }
                else {
                    throw new EvaluationError(_t("At least one measure and/or dimension is not correct."));
                }
            }
            return {
                value: CellErrorType.GenericError,
                message: this.invalidRangeError?.message ??
                    _t("At least one measure and/or dimension is not correct."),
            };
        }
        return undefined;
    }
    areDomainArgsFieldsValid(args) {
        let dimensions = args.filter((_, index) => index % 2 === 0).map(toString);
        if (dimensions.length && dimensions.at(-1) === "measure") {
            dimensions = dimensions.slice(0, -1);
        }
        return areDomainArgsFieldsValid(dimensions, this.definition);
    }
    parseArgsToPivotDomain(args) {
        const domain = [];
        for (let i = 0; i < args.length - 1; i += 2) {
            const fieldWithGranularity = toString(args[i]);
            const type = this.getTypeOfDimension(fieldWithGranularity);
            const normalizedValue = fieldWithGranularity === "measure"
                ? toString(args[i + 1])
                : toNormalizedPivotValue(this.getDimension(fieldWithGranularity), args[i + 1]);
            domain.push({ field: fieldWithGranularity, value: normalizedValue, type });
        }
        return domain;
    }
    markAsDirtyForEvaluation() {
        this.needsReevaluation = true;
    }
    getMeasure(id) {
        return this.definition.getMeasure(id);
    }
    getPivotMeasureValue(id) {
        return {
            value: this.getMeasure(id).displayName,
        };
    }
    getPivotHeaderValueAndFormat(domain) {
        const lastNode = domain.at(-1);
        if (!lastNode) {
            return { value: _t("Total") };
        }
        const dimension = this.getDimension(lastNode.field);
        const cells = this.filterDataEntriesFromDomain(this.dataEntries, domain);
        const finalCell = cells[0]?.[dimension.nameWithGranularity];
        if (dimension.type === "datetime") {
            const adapter = pivotTimeAdapter((dimension.granularity || "month"));
            return adapter.toValueAndFormat(lastNode.value, this.getters.getLocale());
        }
        if (!finalCell) {
            return { value: "" };
        }
        if (finalCell.value === null) {
            return { value: _t("(Undefined)") };
        }
        return finalCell;
    }
    getPivotCellValueAndFormat(measureId, domain) {
        const dataEntries = this.filterDataEntriesFromDomain(this.dataEntries, domain);
        if (dataEntries.length === 0) {
            return { value: "" };
        }
        const measure = this.getMeasure(measureId);
        const allValues = dataEntries.map((value) => value[measure.fieldName]).filter(isDefined);
        const values = allValues.filter((cell) => cell.type !== CellValueType.empty);
        const aggregator = measure.aggregator;
        const operator = AGGREGATORS_FN[aggregator];
        if (!operator) {
            throw new Error(`Aggregator ${aggregator} does not exist`);
        }
        try {
            const result = operator([allValues], this.getters.getLocale());
            if (values.length === 0) {
                return { ...result, value: "" };
            }
            return result;
        }
        catch (e) {
            return handleError(e, aggregator.toUpperCase());
        }
    }
    getPossibleFieldValues(dimension) {
        const values = [];
        const groups = groupPivotDataEntriesBy(this.dataEntries, dimension);
        const orderedKeys = orderDataEntriesKeys(groups, dimension);
        for (const key of orderedKeys) {
            values.push({
                value: groups[key]?.[0]?.[dimension.nameWithGranularity]?.value ?? "",
                label: groups[key]?.[0]?.[dimension.nameWithGranularity]?.formattedValue || "",
            });
        }
        return values;
    }
    getCollapsedTableStructure() {
        if (!this.isValid()) {
            throw new Error("Pivot is not valid !");
        }
        if (!this.collapsedTable) {
            this.collapsedTable = dataEntriesToSpreadsheetPivotTable(this.dataEntries, this.definition, "collapsed");
        }
        return this.collapsedTable;
    }
    getExpandedTableStructure() {
        if (!this.isValid()) {
            throw new Error("Pivot is not valid !");
        }
        if (!this.expandedTable) {
            this.expandedTable = dataEntriesToSpreadsheetPivotTable(this.dataEntries, this.definition, "expanded");
        }
        return this.expandedTable;
    }
    getFields() {
        return this.metaData.fields;
    }
    get fields() {
        return this.getFields();
    }
    loadMetaData() {
        this.invalidRangeError = undefined;
        if (this.coreDefinition.dataSet) {
            const { zone, sheetId } = this.coreDefinition.dataSet;
            const range = this.getters.getRangeFromZone(sheetId, zone);
            try {
                const extractedMetaData = this.extractFieldsFromRange(range);
                const customFields = createCustomFields(this.coreDefinition, extractedMetaData.fields);
                return {
                    ...extractedMetaData,
                    fields: { ...extractedMetaData.fields, ...customFields },
                };
            }
            catch (e) {
                this.invalidRangeError = e;
                return { fields: {}, fieldKeys: [] };
            }
        }
        else {
            this.invalidRangeError = new EvaluationError(_t("The pivot cannot be created because the dataset is missing."));
            return { fields: {}, fieldKeys: [] };
        }
    }
    loadRuntimeDefinition() {
        return new SpreadsheetPivotRuntimeDefinition(this.coreDefinition, this.fields, this.getters);
    }
    loadData() {
        const range = this._definition?.range;
        return this.isValid() && range ? this.extractDataEntriesFromRange(range) : [];
    }
    getTypeOfDimension(fieldWithGranularity) {
        if (fieldWithGranularity === "measure") {
            return "char";
        }
        const { fieldName } = parseDimension(fieldWithGranularity);
        const type = this.fields[fieldName]?.type;
        if (!type) {
            throw new Error(`Field ${fieldName} does not exist`);
        }
        return type;
    }
    filterDataEntriesFromDomain(dataEntries, domain) {
        return domain.reduce((current, acc) => this.filterDataEntriesFromDomainNode(current, acc), dataEntries);
    }
    filterDataEntriesFromDomainNode(dataEntries, domain) {
        const { field, value, type } = domain;
        const { nameWithGranularity } = this.getDimension(field);
        return dataEntries.filter((entry) => {
            const cellValue = entry[nameWithGranularity]?.value;
            if (type === "char") {
                return String(cellValue) === String(value);
            }
            return cellValue === value;
        });
    }
    getDimension(nameWithGranularity) {
        return this.definition.getDimension(nameWithGranularity);
    }
    getTypeFromZone(sheetId, zone) {
        const cells = this.getters.getEvaluatedCellsInZone(sheetId, zone);
        const nonEmptyCells = cells.filter((cell) => !(cell.type === CellValueType.empty || cell.value === ""));
        if (nonEmptyCells.length === 0) {
            return "integer";
        }
        if (nonEmptyCells.every((cell) => cell.type === CellValueType.number && cell.format && isDateTimeFormat(cell.format))) {
            return "datetime";
        }
        if (nonEmptyCells.every((cell) => cell.type === CellValueType.boolean)) {
            return "boolean";
        }
        if (nonEmptyCells.some((cell) => cell.type === CellValueType.text)) {
            return "char";
        }
        return "integer";
    }
    assertCellIsValidField(col, row, cell) {
        if (cell.type === CellValueType.error) {
            throw new EvaluationError(_t("The pivot cannot be created because cell %s contains an error", toXC(col, row)));
        }
        if (cell.type === CellValueType.empty || cell.value === "") {
            throw new EvaluationError(_t("The pivot cannot be created because cell %s is empty", toXC(col, row)));
        }
        if (cell.value === "__count") {
            throw new EvaluationError(_t("The pivot cannot be created because cell %s contains a reserved value", toXC(col, row)));
        }
    }
    /**
     * Create the fields from the given range. It will extract all the fields from
     * the first row of the range.
     */
    extractFieldsFromRange(range) {
        const fields = {};
        const fieldKeys = [];
        const sheetId = range.sheetId;
        const row = range.zone.top;
        for (let col = range.zone.left; col <= range.zone.right; col++) {
            const cell = this.getters.getEvaluatedCell({ sheetId, col, row });
            this.assertCellIsValidField(col, row, cell);
            const field = cell.value?.toString();
            if (field) {
                const type = this.getTypeFromZone(sheetId, {
                    top: range.zone.top + 1,
                    left: col,
                    bottom: range.zone.bottom,
                    right: col,
                });
                const string = getUniquePivotFieldName(field, fields);
                fields[string] = {
                    name: string,
                    type,
                    string,
                    aggregator: type === "integer" ? "sum" : "count",
                };
                fieldKeys.push(string);
            }
        }
        return { fields, fieldKeys };
    }
    extractDataEntriesFromRange(range) {
        const dataEntries = [];
        for (let row = range.zone.top + 1; row <= range.zone.bottom; row++) {
            const zone = { top: row, bottom: row, left: range.zone.left, right: range.zone.right };
            const cells = this.getters.getEvaluatedCellsInZone(range.sheetId, zone);
            const entry = {};
            for (const index in cells) {
                const cell = cells[index];
                const field = this.fields[this.metaData.fieldKeys[index]];
                if (!field) {
                    throw new Error(`Field ${this.metaData.fieldKeys[index]} does not exist`);
                }
                if (cell.value === "") {
                    entry[field.name] = { value: null, type: CellValueType.empty, formattedValue: "" };
                }
                else {
                    entry[field.name] = cell;
                }
            }
            for (const customFieldName in this.definition.customFields || {}) {
                const customField = this.definition.customFields?.[customFieldName];
                if (!customField)
                    continue;
                const baseValue = entry[customField.parentField];
                const parentField = this.fields[customField.parentField];
                if (!baseValue || !parentField) {
                    entry[customFieldName] = { value: null, type: CellValueType.empty, formattedValue: "" };
                    continue;
                }
                const group = customField.groups.find((g) => g.values.some((v) => v === baseValue?.value)) ||
                    customField.groups.find((g) => g.isOtherGroup);
                entry[customFieldName] = {
                    ...baseValue,
                    value: group ? group.name : baseValue.value,
                };
            }
            entry["__count"] = { value: 1, type: CellValueType.number, formattedValue: "1" };
            dataEntries.push(entry);
        }
        const dateDimensions = this.definition.columns
            .concat(this.definition.rows)
            .filter((d) => d.type === "datetime");
        if (dateDimensions.length) {
            const locale = this.getters.getLocale();
            for (const entry of dataEntries) {
                for (const dimension of dateDimensions) {
                    const value = createDate(dimension, entry[dimension.fieldName]?.value || null, this.getters.getLocale());
                    const adapter = pivotTimeAdapter((dimension.granularity || "month"));
                    const { format, value: valueToFormat } = adapter.toValueAndFormat(value, locale);
                    entry[dimension.nameWithGranularity] = {
                        value,
                        type: entry[dimension.fieldName]?.type || CellValueType.empty,
                        format: entry[dimension.fieldName]?.format,
                        formattedValue: formatValue(valueToFormat, { locale, format }),
                    };
                }
            }
        }
        return dataEntries;
    }
}

const pivotRegistry = new Registry();
const dateGranularities = [
    "year",
    "quarter_number",
    "month_number",
    "month",
    "iso_week_number",
    "day_of_month",
    "day",
    "day_of_week",
];
pivotRegistry.add("SPREADSHEET", {
    ui: SpreadsheetPivot,
    definition: SpreadsheetPivotRuntimeDefinition,
    dateGranularities: [...dateGranularities],
    datetimeGranularities: [...dateGranularities, "hour_number", "minute_number", "second_number"],
    isMeasureCandidate: (field) => field.type !== "boolean",
    isGroupable: () => true,
    canHaveCustomGroup: (field) => field.type === "char" && !field.isCustomField,
    adaptRanges: (getters, definition, applyChange) => {
        if (definition.type !== "SPREADSHEET" || !definition.dataSet) {
            return definition;
        }
        const { sheetId, zone } = definition.dataSet;
        const range = getters.getRangeFromZone(sheetId, zone);
        const adaptedRange = adaptPivotRange(range, applyChange);
        if (adaptedRange === range) {
            return definition;
        }
        const dataSet = adaptedRange && {
            sheetId: adaptedRange.sheetId,
            zone: adaptedRange.zone,
        };
        return { ...definition, dataSet };
    },
});
function adaptPivotRange(range, applyChange) {
    if (!range) {
        return undefined;
    }
    const change = applyChange(range);
    switch (change.changeType) {
        case "REMOVE":
            return undefined;
        default:
            return change.range;
    }
}

const pivotProperties = {
    name: _t("See pivot properties"),
    execute(env) {
        const position = env.model.getters.getActivePosition();
        const pivotId = env.model.getters.getPivotIdFromPosition(position);
        env.openSidePanel("PivotSidePanel", { pivotId });
    },
    isVisible: (env) => {
        const position = env.model.getters.getActivePosition();
        const pivotId = env.model.getters.getPivotIdFromPosition(position);
        return (!env.isSmall && pivotId && env.model.getters.isExistingPivot(pivotId)) || false;
    },
    isReadonlyAllowed: true,
    icon: "o-spreadsheet-Icon.PIVOT",
};
const pivotSortingAsc = {
    name: _t("Ascending"),
    execute: (env) => sortPivot(env, env.model.getters.getActivePosition(), "asc"),
    isActive: (env) => env.model.getters.getPivotCellSortDirection(env.model.getters.getActivePosition()) === "asc",
};
const pivotSortingDesc = {
    name: _t("Descending"),
    execute: (env) => sortPivot(env, env.model.getters.getActivePosition(), "desc"),
    isActive: (env) => env.model.getters.getPivotCellSortDirection(env.model.getters.getActivePosition()) === "desc",
};
const noPivotSorting = {
    name: _t("No sorting"),
    execute: (env) => sortPivot(env, env.model.getters.getActivePosition(), "none"),
    isActive: (env) => env.model.getters.getPivotCellSortDirection(env.model.getters.getActivePosition()) === "none",
};
const FIX_FORMULAS = {
    name: _t("Convert to individual formulas"),
    execute(env) {
        const position = env.model.getters.getActivePosition();
        const cell = env.model.getters.getCorrespondingFormulaCell(position);
        const pivotId = env.model.getters.getPivotIdFromPosition(position);
        if (!cell || !pivotId) {
            return;
        }
        const { sheetId, col, row } = env.model.getters.getCellPosition(cell.id);
        const pivot = env.model.getters.getPivot(pivotId);
        pivot.init();
        if (!pivot.isValid()) {
            return;
        }
        env.model.dispatch("SPLIT_PIVOT_FORMULA", {
            sheetId,
            col,
            row,
            pivotId,
        });
    },
    isVisible: (env) => {
        const position = env.model.getters.getActivePosition();
        const pivotId = env.model.getters.getPivotIdFromPosition(position);
        if (!pivotId) {
            return false;
        }
        const pivot = env.model.getters.getPivot(pivotId);
        const cell = env.model.getters.getEvaluatedCell(position);
        return (pivot.isValid() &&
            env.model.getters.isSpillPivotFormula(position) &&
            cell.type !== CellValueType.error);
    },
    icon: "o-spreadsheet-Icon.PIVOT",
};
const groupPivotHeaders = {
    name: _t("Group pivot dimensions"),
    execute: (env) => {
        const matchingHeaders = getMatchingPivotHeadersInSelection(env);
        if (!matchingHeaders) {
            return;
        }
        const { pivotId, values, field } = matchingHeaders;
        const pivot = env.model.getters.getPivot(pivotId);
        const definition = deepCopy(env.model.getters.getPivotCoreDefinition(pivotId));
        if (!field.isCustomField) {
            groupValuesInNormalField(definition, values, field, pivot.getFields());
        }
        else {
            const customField = (definition.customFields || {})[field.name];
            if (!customField) {
                return;
            }
            groupValuesInCustomField(customField, values);
        }
        env.model.dispatch("UPDATE_PIVOT", { pivotId, pivot: definition });
    },
    isVisible: (env) => {
        const matchingHeaders = getMatchingPivotHeadersInSelection(env);
        if (!matchingHeaders) {
            return false;
        }
        const { pivotId, values, field } = matchingHeaders;
        const pivot = env.model.getters.getPivot(pivotId);
        return (values.length > 1 &&
            (field.isCustomField || pivotRegistry.get(pivot.type).canHaveCustomGroup(field)));
    },
};
const groupRemainingPivotHeadersAction = {
    name: _t("Group all remaining dimensions"),
    execute: (env) => {
        const matchingHeaders = getMatchingPivotHeadersInSelection(env);
        if (!matchingHeaders) {
            return;
        }
        const { pivotId, field } = matchingHeaders;
        const pivot = env.model.getters.getPivot(pivotId);
        const definition = deepCopy(env.model.getters.getPivotCoreDefinition(pivotId));
        const customField = field.isCustomField
            ? (definition.customFields || {})[field.name]
            : getCustomFieldWithParentField(definition, field, pivot.getFields());
        if (!customField) {
            return;
        }
        customField.groups.push({
            name: getUniquePivotGroupName(_t("Others"), customField),
            values: [],
            isOtherGroup: true,
        });
        addDimensionToPivotDefinition(definition, field.name, customField.name);
        env.model.dispatch("UPDATE_PIVOT", { pivotId, pivot: definition });
    },
    isVisible: (env) => {
        const matchingHeaders = getMatchingPivotHeadersInSelection(env);
        if (!matchingHeaders) {
            return false;
        }
        const { pivotId, field, values } = matchingHeaders;
        return valuesAreAllNonGroupedValues(env, pivotId, values, field);
    },
};
const ungroupPivotHeadersAction = {
    name: _t("Ungroup pivot dimensions"),
    execute: (env) => {
        const matchingHeaders = getMatchingPivotHeadersInSelection(env);
        if (!matchingHeaders) {
            return;
        }
        const { pivotId, values, field } = matchingHeaders;
        const pivot = env.model.getters.getPivot(pivotId);
        const definition = deepCopy(env.model.getters.getPivotCoreDefinition(pivotId));
        ungroupPivotHeaders(definition, values, field, pivot.getFields());
        env.model.dispatch("UPDATE_PIVOT", { pivotId, pivot: definition });
    },
    isVisible: (env) => {
        const matchingHeaders = getMatchingPivotHeadersInSelection(env);
        if (!matchingHeaders) {
            return false;
        }
        const { pivotId, values, field } = matchingHeaders;
        const pivot = env.model.getters.getPivot(pivotId);
        const definition = env.model.getters.getPivotCoreDefinition(pivotId);
        if (!field.isCustomField) {
            // Check if the parent custom grouped field is in the pivot
            const customField = getCustomFieldWithParentField(definition, field, pivot.getFields());
            const dimensions = [...definition.rows, ...definition.columns];
            if (!dimensions.some((d) => d.fieldName === customField.name)) {
                return false;
            }
        }
        return areFieldValuesInGroups(definition, values, field, pivot.getFields());
    },
};
function canSortPivot(getters, position) {
    const pivotId = getters.getPivotIdFromPosition(position);
    if (!pivotId || !getters.isExistingPivot(pivotId) || !getters.isSpillPivotFormula(position)) {
        return false;
    }
    const pivot = getters.getPivot(pivotId);
    if (!pivot.isValid()) {
        return false;
    }
    const pivotCell = getters.getPivotCellFromPosition(position);
    return pivotCell.type === "VALUE" || pivotCell.type === "MEASURE_HEADER";
}
function sortPivot(env, position, order) {
    const pivotId = env.model.getters.getPivotIdFromPosition(position);
    const pivotCell = env.model.getters.getPivotCellFromPosition(position);
    if (pivotCell.type === "EMPTY" || pivotCell.type === "HEADER" || !pivotId) {
        return;
    }
    if (order === "none") {
        env.model.dispatch("UPDATE_PIVOT", {
            pivotId: pivotId,
            pivot: {
                ...env.model.getters.getPivotCoreDefinition(pivotId),
                sortedColumn: undefined,
            },
        });
        return;
    }
    const pivot = env.model.getters.getPivot(pivotId);
    const colDomain = domainToColRowDomain(pivot, pivotCell.domain).colDomain;
    env.model.dispatch("UPDATE_PIVOT", {
        pivotId: pivotId,
        pivot: {
            ...env.model.getters.getPivotCoreDefinition(pivotId),
            sortedColumn: { domain: colDomain, order, measure: pivotCell.measure },
        },
    });
}
/*
 * Get the values of the pivot headers in the current selection, if all the pivot headers on the selection belong
 * to the same pivot, the same field and that the pivot formula is a dynamic pivot. Otherwise return undefined.
 */
function getMatchingPivotHeadersInSelection(env) {
    let pivotId;
    let fieldName;
    const pivotHeaders = [];
    for (const zone of env.model.getters.getSelectedZones()) {
        const sheetId = env.model.getters.getActiveSheetId();
        for (const position of cellPositions(sheetId, zone)) {
            const cellPivotId = env.model.getters.getPivotIdFromPosition(position);
            if (!pivotId) {
                pivotId = cellPivotId;
            }
            else if (cellPivotId && pivotId !== cellPivotId) {
                return undefined;
            }
            if (!pivotId) {
                continue;
            }
            const pivotCell = env.model.getters.getPivotCellFromPosition(position);
            if (pivotCell.type !== "HEADER" || !env.model.getters.isSpillPivotFormula(position)) {
                continue;
            }
            const cellLeafField = pivotCell.domain.at(-1)?.field;
            if (!fieldName && cellLeafField) {
                fieldName = cellLeafField;
            }
            else if (fieldName && cellLeafField && fieldName !== cellLeafField) {
                return undefined;
            }
            pivotHeaders.push(pivotCell);
        }
    }
    if (!pivotId || !fieldName || pivotHeaders.length === 0) {
        return undefined;
    }
    const field = env.model.getters.getPivot(pivotId).getFields()[fieldName];
    if (!field) {
        return undefined;
    }
    const values = pivotHeaders
        .map((pivotCell) => pivotCell.domain.at(-1)?.value)
        .filter((val) => val !== undefined);
    return { pivotId, values, field };
}
/**
 *  Remove existing groups containing the selected values, and create a new group
 */
function groupValuesInNormalField(definition, selectedValues, field, fields) {
    const customField = getCustomFieldWithParentField(definition, field, fields);
    removePivotGroupsContainingValues(selectedValues, customField);
    const newGroup = {
        name: getUniquePivotGroupName(_t("Group"), customField),
        values: selectedValues,
    };
    customField.groups.push(newGroup);
    if (!definition.customFields) {
        definition.customFields = {};
    }
    definition.customFields[customField.name] = customField;
    addDimensionToPivotDefinition(definition, field.name, customField.name);
}
/**
 * We either merge the selected values into a single existing group, or create a new group
 */
function groupValuesInCustomField(customField, fieldValues) {
    const valuesToGroup = new Set();
    const groupsInSelection = [];
    for (const value of fieldValues) {
        const group = customField.groups.find((g) => g.name === value);
        if (group) {
            groupsInSelection.push(group);
            group.values.forEach((v) => valuesToGroup.add(v));
        }
        else {
            valuesToGroup.add(value);
        }
    }
    if (groupsInSelection.some((g) => g.isOtherGroup)) {
        customField.groups = customField.groups.filter((g) => g.isOtherGroup || !groupsInSelection.includes(g));
    }
    else if (groupsInSelection.length === 0) {
        const newGroup = {
            name: getUniquePivotGroupName(_t("Group"), customField),
            values: fieldValues,
        };
        customField.groups.push(newGroup);
    }
    else {
        const groupsToRemove = groupsInSelection.slice(1);
        customField.groups = customField.groups.filter((g) => !groupsToRemove.includes(g));
        groupsInSelection[0].values = Array.from(valuesToGroup);
    }
}
function ungroupPivotHeaders(definition, fieldValues, field, fields) {
    let customField;
    // Non-custom field: remove existing groups containing the selected values
    if (!field.isCustomField) {
        customField = getCustomFieldWithParentField(definition, field, fields);
        // Check if some values are in the  "Others" group
        if (customField.groups.some((g) => g.isOtherGroup) &&
            fieldValues.some((v) => !customField?.groups.some((g) => g.values.includes(v)))) {
            customField.groups = customField.groups.filter((g) => !g.isOtherGroup);
        }
        removePivotGroupsContainingValues(fieldValues, customField);
    }
    // Custom field: remove the selected groups
    else {
        customField = (definition.customFields || {})[field.name];
        if (!customField) {
            return;
        }
        customField.groups = customField.groups.filter((g) => !fieldValues.includes(g.name));
    }
    if (customField.groups.every((g) => g.values.length === 0 && !g.isOtherGroup)) {
        removeCustomFieldFromDimensions(definition, customField.name);
        delete definition.customFields?.[customField.name];
    }
}
/**
 * Checks if the given field values are in any of the groups of the given field.
 */
function areFieldValuesInGroups(definition, fieldValues, field, fields) {
    // If the field is not a custom field, check if there are any custom groups containing the selected values
    if (!field.isCustomField) {
        const customField = getCustomFieldWithParentField(definition, field, fields);
        return customField.groups.some((group) => group.isOtherGroup || fieldValues.some((value) => group.values.includes(value)));
    }
    // If the field is a custom field, check if there are any groups named after the selected values
    else {
        const customField = (definition.customFields || {})[field.name];
        if (!customField) {
            return false;
        }
        return customField.groups.some((group) => fieldValues.includes(group.name));
    }
}
/** Checks that the values given are equal to all the values that are not grouped in the pivot dimension. */
function valuesAreAllNonGroupedValues(env, pivotId, values, field) {
    const pivot = env.model.getters.getPivot(pivotId);
    const definition = env.model.getters.getPivotCoreDefinition(pivotId);
    const customField = field.isCustomField
        ? (definition.customFields || {})[field.name]
        : Object.values(definition.customFields || {}).find((f) => f.parentField === field.name);
    const dimension = pivot.definition.getDimension(field.name);
    if (!dimension ||
        !customField ||
        areFieldValuesInGroups(definition, values, field, pivot.getFields())) {
        return false;
    }
    const possibleValues = new Set(pivot.getPossibleFieldValues(dimension).map((v) => v.value));
    const groupValues = field.isCustomField
        ? customField.groups.map((g) => g.name)
        : customField.groups.flatMap((g) => g.values);
    for (const val of [...values, ...groupValues]) {
        possibleValues.delete(val);
    }
    return possibleValues.size === 0;
}
/**
 * Remove the given custom field from the rows/columns of the pivot, and replace it by the field it's based on (if the
 * field isn't already in the pivot). Modifies the definition in place.
 */
function removeCustomFieldFromDimensions(definition, customFieldName) {
    const customField = definition.customFields?.[customFieldName];
    if (!customField) {
        return;
    }
    const isParentFieldInDimensions = [...definition.rows, ...definition.columns].some((d) => d.fieldName === customField.parentField);
    for (const dim of [definition.rows, definition.columns]) {
        const indexInDimension = dim.findIndex((d) => d.fieldName === customFieldName);
        if (indexInDimension !== -1) {
            if (!isParentFieldInDimensions) {
                dim.splice(indexInDimension, 1, { fieldName: customField.parentField });
            }
            else {
                dim.splice(indexInDimension, 1);
            }
        }
    }
}

//------------------------------------------------------------------------------
// Context MenuPopover Registry
//------------------------------------------------------------------------------
const cellMenuRegistry = new MenuItemRegistry();
cellMenuRegistry
    .add("cut", {
    ...cut,
    sequence: 10,
})
    .add("copy", {
    ...copy,
    sequence: 20,
})
    .add("paste", {
    ...paste,
    sequence: 30,
})
    .add("paste_special", {
    ...pasteSpecial,
    sequence: 40,
    separator: true,
})
    .addChild("paste_value_only", ["paste_special"], {
    ...pasteSpecialValue,
    sequence: 10,
})
    .addChild("paste_format_only", ["paste_special"], {
    ...pasteSpecialFormat,
    sequence: 20,
})
    .add("add_row_before", {
    ...cellInsertRowsBefore,
    sequence: 70,
})
    .add("add_column_before", {
    ...cellInsertColsBefore,
    sequence: 90,
})
    .add("insert_cell", {
    ...insertCell,
    sequence: 100,
    separator: true,
})
    .addChild("insert_cell_down", ["insert_cell"], {
    ...insertCellShiftDown,
    name: _t("Shift down"),
    sequence: 10,
})
    .addChild("insert_cell_right", ["insert_cell"], {
    ...insertCellShiftRight,
    name: _t("Shift right"),
    sequence: 20,
})
    .add("delete_row", {
    ...deleteRow,
    sequence: 110,
    icon: "o-spreadsheet-Icon.TRASH",
})
    .add("delete_column", {
    ...deleteCol,
    sequence: 120,
    icon: "o-spreadsheet-Icon.TRASH",
})
    .add("delete_cell", {
    ...deleteCells,
    sequence: 130,
    separator: true,
    icon: "o-spreadsheet-Icon.TRASH",
})
    .addChild("delete_cell_up", ["delete_cell"], {
    ...deleteCellShiftUp,
    name: _t("Shift up"),
    sequence: 10,
    icon: "o-spreadsheet-Icon.DELETE_CELL_SHIFT_UP",
})
    .addChild("delete_cell_left", ["delete_cell"], {
    ...deleteCellShiftLeft,
    name: _t("Shift left"),
    sequence: 20,
    icon: "o-spreadsheet-Icon.DELETE_CELL_SHIFT_LEFT",
})
    .add("edit_table", {
    ...editTable,
    isVisible: SELECTION_CONTAINS_SINGLE_TABLE,
    isEnabled: (env) => !env.isSmall,
    sequence: 140,
})
    .add("delete_table", {
    ...deleteTable,
    isVisible: SELECTION_CONTAINS_SINGLE_TABLE,
    sequence: 145,
    separator: true,
})
    .add("insert_link", {
    ...insertLink,
    name: INSERT_LINK_NAME,
    sequence: 150,
    separator: true,
})
    .add("pivot_headers_group", {
    sequence: 155,
    icon: "o-spreadsheet-Icon.PLUS_IN_BOX",
    ...groupPivotHeaders,
})
    .add("pivot_group_remaining", {
    sequence: 155,
    icon: "o-spreadsheet-Icon.PLUS_IN_BOX",
    ...groupRemainingPivotHeadersAction,
})
    .add("pivot_headers_ungroup", {
    sequence: 155,
    icon: "o-spreadsheet-Icon.MINUS_IN_BOX",
    ...ungroupPivotHeadersAction,
})
    .add("pivot_sorting", {
    name: _t("Sort pivot"),
    sequence: 155,
    icon: "o-spreadsheet-Icon.SORT_RANGE",
    isVisible: (env) => {
        const position = env.model.getters.getActivePosition();
        return canSortPivot(env.model.getters, position);
    },
})
    .add("pivot_fix_formulas", {
    ...FIX_FORMULAS,
    sequence: 160,
})
    .add("pivot_properties", {
    ...pivotProperties,
    sequence: 170,
    separator: true,
})
    .addChild("pivot_sorting_asc", ["pivot_sorting"], {
    ...pivotSortingAsc,
    sequence: 10,
})
    .addChild("pivot_sorting_desc", ["pivot_sorting"], {
    ...pivotSortingDesc,
    sequence: 20,
})
    .addChild("pivot_sorting_none", ["pivot_sorting"], {
    ...noPivotSorting,
    sequence: 30,
});

const sortRange = {
    name: _t("Sort range"),
    isVisible: IS_ONLY_ONE_RANGE,
    icon: "o-spreadsheet-Icon.SORT_RANGE",
};
const sortAscending = {
    name: _t("Ascending (A ⟶ Z)"),
    execute: (env) => {
        const { anchor, zones } = env.model.getters.getSelection();
        const sheetId = env.model.getters.getActiveSheetId();
        interactiveSortSelection(env, sheetId, anchor.cell, zones[0], "asc");
    },
    icon: "o-spreadsheet-Icon.SORT_ASCENDING",
};
const dataCleanup = {
    name: _t("Data cleanup"),
    icon: "o-spreadsheet-Icon.DATA_CLEANUP",
};
const removeDuplicates = {
    name: _t("Remove duplicates"),
    execute: (env) => {
        if (getZoneArea(env.model.getters.getSelectedZone()) === 1) {
            env.model.selection.selectTableAroundSelection();
        }
        env.openSidePanel("RemoveDuplicates", {});
    },
    isEnabled: (env) => !env.isSmall,
};
const trimWhitespace = {
    name: _t("Trim whitespace"),
    execute: (env) => {
        env.model.dispatch("TRIM_WHITESPACE");
    },
};
const sortDescending = {
    name: _t("Descending (Z ⟶ A)"),
    execute: (env) => {
        const { anchor, zones } = env.model.getters.getSelection();
        const sheetId = env.model.getters.getActiveSheetId();
        interactiveSortSelection(env, sheetId, anchor.cell, zones[0], "desc");
    },
    icon: "o-spreadsheet-Icon.SORT_DESCENDING",
};
const createRemoveFilter = {
    ...CREATE_OR_REMOVE_FILTER_ACTION,
};
const createRemoveFilterTool = {
    ...CREATE_OR_REMOVE_FILTER_ACTION,
    isActive: (env) => SELECTED_TABLE_HAS_FILTERS(env),
};
const splitToColumns = {
    name: _t("Split text to columns"),
    sequence: 1,
    execute: (env) => env.openSidePanel("SplitToColumns", {}),
    isEnabled: (env) => !env.isSmall && env.model.getters.isSingleColSelected(),
    icon: "o-spreadsheet-Icon.SPLIT_TEXT",
};
const reinsertDynamicPivotMenu = {
    id: "reinsert_dynamic_pivot",
    name: _t("Re-insert dynamic pivot"),
    sequence: 60,
    icon: "o-spreadsheet-Icon.INSERT_PIVOT",
    children: [REINSERT_DYNAMIC_PIVOT_CHILDREN],
    isVisible: (env) => env.model.getters.getPivotIds().some((id) => env.model.getters.getPivot(id).isValid()),
};
const reinsertStaticPivotMenu = {
    id: "reinsert_static_pivot",
    name: _t("Re-insert static pivot"),
    sequence: 70,
    icon: "o-spreadsheet-Icon.INSERT_PIVOT",
    children: [REINSERT_STATIC_PIVOT_CHILDREN],
    isVisible: (env) => env.model.getters.getPivotIds().some((id) => env.model.getters.getPivot(id).isValid()),
};

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

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

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

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

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

class ArrayFormulaHighlight extends SpreadsheetStore {
    highlightStore = this.get(HighlightStore);
    constructor(get) {
        super(get);
        this.highlightStore.register(this);
    }
    get highlights() {
        const position = this.model.getters.getActivePosition();
        const spreader = this.model.getters.getArrayFormulaSpreadingOn(position);
        const zone = spreader
            ? this.model.getters.getSpreadZone(spreader, { ignoreSpillError: true })
            : this.model.getters.getSpreadZone(position, { ignoreSpillError: true });
        if (!zone) {
            return [];
        }
        const isArrayFormulaBlocked = this.model.getters.isArrayFormulaSpillBlocked(spreader ?? position);
        return [
            {
                range: this.model.getters.getRangeFromZone(position.sheetId, zone),
                dashed: isArrayFormulaBlocked,
                color: "#17A2B8",
                noFill: true,
                thinLine: true,
            },
        ];
    }
}

class ClientFocusStore extends SpreadsheetStore {
    mutators = [
        "focusClient",
        "unfocusClient",
        "showClientTag",
        "hideClientTag",
        "jumpToClient",
    ];
    _showClientTag = false;
    clientFocusTimeout = {};
    constructor(get) {
        super(get);
        this.onDispose(() => {
            for (const clientId in this.clientFocusTimeout) {
                this.unfocusClient(clientId);
            }
        });
    }
    get focusedClients() {
        const focused = new Set();
        this.model.getters.getConnectedClients().forEach((client) => {
            if (this._showClientTag || this.clientFocusTimeout[client.id] !== undefined) {
                focused.add(client.id);
            }
        });
        return focused;
    }
    jumpToClient(clientId) {
        const client = this.model.getters.getClient(clientId);
        this.focusClient(clientId);
        if (client.position) {
            this.model.dispatch("ACTIVATE_SHEET", {
                sheetIdTo: client.position.sheetId,
                sheetIdFrom: this.getters.getActiveSheetId(),
            });
            this.model.dispatch("SCROLL_TO_CELL", { col: client.position.col, row: client.position.row });
        }
    }
    showClientTag() {
        this._showClientTag = true;
    }
    hideClientTag() {
        this._showClientTag = false;
    }
    focusClient(clientId) {
        if (this.clientFocusTimeout[clientId]) {
            clearTimeout(this.clientFocusTimeout[clientId]);
        }
        // This call to unfocus client isn't proxyfied and doesn't trigger a render.
        // The focus will visually disappear when the next render is triggered
        this.clientFocusTimeout[clientId] = setTimeout(() => this.unfocusClient(clientId), 3000);
    }
    unfocusClient(clientId) {
        if (this.clientFocusTimeout[clientId]) {
            clearTimeout(this.clientFocusTimeout[clientId]);
        }
        this.clientFocusTimeout[clientId] = undefined;
    }
}

/**
 * Function to be used during a pointerdown event, this function allows to
 * perform actions related to the pointermove and pointerup events and adjusts the viewport
 * when the new position related to the pointermove event is outside of it.
 * Among inputs are two callback functions. First intended for actions performed during
 * the pointermove event, it receives as parameters the current position of the pointermove
 * (occurrence of the current column and the current row). Second intended for actions
 * performed during the pointerup event.
 */
function useDragAndDropBeyondTheViewport(env) {
    let timeOutId = null;
    let currentEv;
    let previousEvClientPosition;
    let startingX;
    let startingY;
    let scrollDirection = "all";
    const getters = env.model.getters;
    let cleanUpFns = [];
    const cleanUp = () => {
        clearTimeout(timeOutId);
        timeOutId = null;
        cleanUpFns.forEach((fn) => fn());
        cleanUpFns = [];
    };
    let pointerMoveCallback;
    let pointerUpCallback;
    const pointerMoveHandler = (ev) => {
        currentEv = ev;
        if (timeOutId) {
            return;
        }
        const sheetId = getters.getActiveSheetId();
        const position = gridOverlayPosition();
        const { x: offsetCorrectionX, y: offsetCorrectionY } = getters.getMainViewportCoordinates();
        const { top, left, bottom, right } = getters.getActiveMainViewport();
        let { scrollX, scrollY } = getters.getActiveSheetScrollInfo();
        const { xSplit, ySplit } = getters.getPaneDivisions(sheetId);
        let canEdgeScroll = false;
        let timeoutDelay = MAX_DELAY;
        const x = currentEv.clientX - position.left;
        let colIndex = getters.getColIndex(x);
        if (scrollDirection !== "vertical") {
            const previousX = previousEvClientPosition.clientX - position.left;
            const edgeScrollInfoX = getters.getEdgeScrollCol(x, previousX, startingX);
            if (edgeScrollInfoX.canEdgeScroll) {
                canEdgeScroll = true;
                timeoutDelay = Math.min(timeoutDelay, edgeScrollInfoX.delay);
                let newTarget = colIndex;
                switch (edgeScrollInfoX.direction) {
                    case "reset":
                        colIndex = 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;
        let rowIndex = getters.getRowIndex(y);
        if (scrollDirection !== "horizontal") {
            const previousY = previousEvClientPosition.clientY - position.top;
            const edgeScrollInfoY = getters.getEdgeScrollRow(y, previousY, startingY);
            if (edgeScrollInfoY.canEdgeScroll) {
                canEdgeScroll = true;
                timeoutDelay = Math.min(timeoutDelay, edgeScrollInfoY.delay);
                let newTarget = rowIndex;
                switch (edgeScrollInfoY.direction) {
                    case "reset":
                        rowIndex = newTarget = ySplit;
                        break;
                    case 1:
                        rowIndex = bottom;
                        newTarget = top + 1;
                        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) {
            colIndex = adjustIndexWithinBounds(colIndex, x, getters.getNumberCols(sheetId) - 1);
            rowIndex = adjustIndexWithinBounds(rowIndex, y, getters.getNumberRows(sheetId) - 1);
        }
        pointerMoveCallback?.(colIndex, rowIndex, currentEv);
        if (canEdgeScroll) {
            env.model.dispatch("SET_VIEWPORT_OFFSET", { offsetX: scrollX, offsetY: scrollY });
            timeOutId = setTimeout(() => {
                timeOutId = null;
                pointerMoveHandler(currentEv);
            }, Math.round(timeoutDelay));
        }
        previousEvClientPosition = { clientX: currentEv.clientX, clientY: currentEv.clientY };
    };
    const pointerUpHandler = () => {
        pointerUpCallback?.();
        cleanUp();
    };
    const startFn = (initialPointerCoordinates, onPointerMove, onPointerUp, startScrollDirection = "all") => {
        cleanUp();
        const position = gridOverlayPosition();
        scrollDirection = startScrollDirection;
        startingX = initialPointerCoordinates.clientX - position.left;
        startingY = initialPointerCoordinates.clientY - position.top;
        previousEvClientPosition = {
            clientX: initialPointerCoordinates.clientX,
            clientY: initialPointerCoordinates.clientY,
        };
        pointerMoveCallback = onPointerMove;
        pointerUpCallback = onPointerUp;
        cleanUpFns.push(startDnd(pointerMoveHandler, pointerUpHandler));
    };
    onWillUnmount(() => {
        cleanUp();
    });
    useEffect(() => {
        cleanUp();
    }, () => [getters.getActiveSheetId()]);
    return { start: startFn };
}
function adjustIndexWithinBounds(index, position, max) {
    if (index === -1) {
        return position < 0 ? 0 : max;
    }
    return index;
}

// -----------------------------------------------------------------------------
// Autofill
// -----------------------------------------------------------------------------
css /* scss */ `
  .o-autofill {
    position: absolute;
    height: ${AUTOFILL_EDGE_LENGTH}px;
    width: ${AUTOFILL_EDGE_LENGTH}px;
    border: 1px solid white;
    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 Component {
    static template = "o-spreadsheet-Autofill";
    static props = {
        position: Object,
        isVisible: Boolean,
    };
    state = useState({
        position: { left: 0, top: 0 },
        handler: false,
    });
    dragNDropGrid = useDragAndDropBeyondTheViewport(this.env);
    get style() {
        const { left, top } = this.props.position;
        return cssPropertiesToCss({
            top: `${top}px`,
            left: `${left}px`,
            visibility: this.props.isVisible ? "visible" : "hidden",
        });
    }
    get handlerStyle() {
        const { left, top } = this.state.handler ? this.state.position : this.props.position;
        return cssPropertiesToCss({
            top: `${top}px`,
            left: `${left}px`,
        });
    }
    get styleNextValue() {
        const { left, top } = this.state.position;
        return cssPropertiesToCss({
            top: `${top + 5}px`,
            left: `${left + 15}px`,
        });
    }
    getTooltip() {
        const tooltip = this.env.model.getters.getAutofillTooltip();
        if (tooltip && !tooltip.component) {
            tooltip.component = TooltipComponent;
        }
        return tooltip;
    }
    onMouseDown(ev) {
        this.state.handler = true;
        let lastCol;
        let lastRow;
        const start = {
            left: ev.clientX - this.props.position.left,
            top: ev.clientY - this.props.position.top,
        };
        const onMouseUp = () => {
            this.state.handler = false;
            this.state.position = { ...this.props.position };
            this.env.model.dispatch("AUTOFILL");
        };
        const onMouseMove = (col, row, ev) => {
            this.state.position = {
                left: ev.clientX - start.left,
                top: ev.clientY - start.top,
            };
            if (lastCol !== col || lastRow !== row) {
                const activeSheetId = this.env.model.getters.getActiveSheetId();
                const numberOfCols = this.env.model.getters.getNumberCols(activeSheetId);
                const numberOfRows = this.env.model.getters.getNumberRows(activeSheetId);
                lastCol = col === -1 ? lastCol : clip(col, 0, numberOfCols);
                lastRow = row === -1 ? lastRow : clip(row, 0, numberOfRows);
                if (lastCol !== undefined && lastRow !== undefined) {
                    this.env.model.dispatch("AUTOFILL_SELECT", { col: lastCol, row: lastRow });
                }
            }
        };
        this.dragNDropGrid.start(ev, onMouseMove, onMouseUp);
    }
    onDblClick() {
        this.env.model.dispatch("AUTOFILL_AUTO");
    }
}
class TooltipComponent extends Component {
    static props = {
        content: String,
    };
    static template = 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;
    pointer-events: none;
  }
`;
class ClientTag extends Component {
    static template = "o-spreadsheet-ClientTag";
    static props = {
        active: Boolean,
        name: String,
        color: String,
        col: Number,
        row: Number,
    };
    get tagStyle() {
        const { col, row, color } = this.props;
        const { height } = this.env.model.getters.getSheetViewDimensionWithHeaders();
        const visible = this.env.model.getters.isVisibleInViewport({
            sheetId: this.env.model.getters.getActiveSheetId(),
            col,
            row,
        });
        const { x, y } = this.env.model.getters.getVisibleRect({
            left: col,
            top: row,
            right: col,
            bottom: row,
        });
        return cssPropertiesToCss({
            bottom: `${height - y + 15}px`,
            left: `${x - 1}px`,
            border: `1px solid ${color}`,
            "background-color": color,
            visibility: visible ? "visible" : "hidden",
        });
    }
}

const ASSOCIATIVE_OPERATORS = ["*", "+", "&"];
/**
 * Pretty-prints formula ASTs into readable formulas.
 *
 * Implements a Wadler-inspired pretty printer:
 * it converts an AST into a `Doc` structure,
 * and then chooses between compact (flat) or expanded (with
 * line breaks and indentation) layouts depending on space.
 *
 * References:
 * - https://lik.ai/blog/how-a-pretty-printer-works/
 * - Wadler, "A prettier printer": https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf
 */
function prettify(ast, width = 60) {
    return "=" + print(astToDoc(ast), width - 1); // width-1 because of the leading '='
}
/**
 * A possible line break.
 * Printed as either space or newline.
 */
function line() {
    return { type: "insertLine" };
}
/**
 * Increase indentation for a nested block.
 */
function nest(indentLevel, doc) {
    return { type: "nest", indentLevel, doc };
}
/**
 * Combines multiple docs into a single doc, concatenating them side by side
 * without any line breaks or indentation.
 */
function concat(docs) {
    return { type: "concat", docs };
}
/**
 * Marks a document as a unit to be printed "flat" (all on one line)
 * if it fits within the width, otherwise with line breaks.
 */
function group(doc) {
    return chooseBetween(flatten(doc), doc);
}
/**
 * Creates a choice between two alternative layouts.
 * The formatter tries `doc1`; if it does not fit within
 * the line width, it falls back to `doc2`.
 */
function chooseBetween(doc1, doc2) {
    return { type: "chooseBetween", doc1, doc2 };
}
/**
 * Flattens a doc into its single-line form.
 */
function flatten(doc) {
    if (typeof doc === "string") {
        return doc;
    }
    if (doc.type === "chooseBetween") {
        // Normally should be "chooseBetween(flatten(doc.doc1), flatten(doc.doc2))",
        // but this is simplified for performance reasons.
        return flatten(doc.doc1);
    }
    if (doc.type === "concat") {
        return concat(doc.docs.map(flatten));
    }
    if (doc.type === "nest") {
        return {
            type: "nest",
            indentLevel: doc.indentLevel,
            doc: flatten(doc.doc),
        };
    }
    if (doc.type === "insertLine") {
        return "";
    }
    return doc;
}
const getIndentationString = memoize(function getIndentationString(indentLevel) {
    return "\n" + "\t".repeat(indentLevel);
});
/**
 * Converts a `Doc` into a string representation that fits within
 * the specified width.
 */
function print(doc, width) {
    return stringify(selectBestLayout(width, doc));
}
/**
 * Join all segments of a LinkedString into the final string.
 */
function stringify(linkedString) {
    let result = "";
    while (linkedString) {
        result += linkedString.subString;
        linkedString = linkedString.next;
    }
    return result;
}
/**
 * Layout selection for a `Doc` that fits within the given width.
 */
function selectBestLayout(width, doc) {
    const head = {
        indentLevel: 0,
        doc,
        next: null,
    };
    return _selectBestLayout(width, 0, head);
}
function _selectBestLayout(width, currentIndentLevel, head) {
    if (head === null) {
        return null;
    }
    const { indentLevel, doc, next } = head;
    if (typeof doc === "string") {
        return {
            subString: doc,
            next: _selectBestLayout(width, currentIndentLevel + doc.length, next),
        };
    }
    if (doc.type === "concat") {
        let newHead = next;
        for (let i = doc.docs.length - 1; i >= 0; i--) {
            newHead = { indentLevel, doc: doc.docs[i], next: newHead };
        }
        return _selectBestLayout(width, currentIndentLevel, newHead);
    }
    if (doc.type === "nest") {
        return _selectBestLayout(width, currentIndentLevel, {
            indentLevel: indentLevel + doc.indentLevel,
            doc: doc.doc,
            next,
        });
    }
    if (doc.type === "insertLine") {
        return {
            subString: getIndentationString(indentLevel),
            next: _selectBestLayout(width, indentLevel, next),
        };
    }
    if (doc.type === "chooseBetween") {
        const head1 = { indentLevel, doc: doc.doc1, next };
        const possibleLinkedString = _selectBestLayout(width, currentIndentLevel, head1);
        if (fits(width - currentIndentLevel, possibleLinkedString)) {
            return possibleLinkedString;
        }
        const head2 = { indentLevel, doc: doc.doc2, next };
        return _selectBestLayout(width, currentIndentLevel, head2);
    }
    return null;
}
/**
 * Check if a layout fits on a single line within width.
 */
function fits(width, linkedString) {
    while (linkedString) {
        if (linkedString.subString[0] === "\n") {
            return true;
        }
        width -= linkedString.subString.length;
        if (width < 0)
            return false;
        linkedString = linkedString.next;
    }
    return true;
}
function astToDoc(ast) {
    switch (ast.type) {
        case "NUMBER":
            return String(ast.value);
        case "STRING":
            return `"${ast.value}"`;
        case "BOOLEAN":
            return ast.value ? "TRUE" : "FALSE";
        case "REFERENCE":
            return ast.value;
        case "FUNCALL":
            const docs = ast.args.map(astToDoc);
            return wrapInParentheses(concat(docs.map((doc, i) => (i < 1 ? doc : concat([", ", line(), doc])))), ast.value);
        case "UNARY_OPERATION":
            const operandDoc = astToDoc(ast.operand);
            const needParenthesis = ast.postfix
                ? leftOperandNeedsParenthesis(ast)
                : rightOperandNeedsParenthesis(ast);
            const finalOperandDoc = needParenthesis ? wrapInParentheses(operandDoc) : operandDoc;
            return ast.postfix
                ? concat([finalOperandDoc, ast.value])
                : concat([ast.value, finalOperandDoc]);
        case "BIN_OPERATION": {
            const leftDoc = astToDoc(ast.left);
            const needParenthesisLeftDoc = leftOperandNeedsParenthesis(ast);
            const finalLeftDoc = needParenthesisLeftDoc ? wrapInParentheses(leftDoc) : leftDoc;
            const rightDoc = astToDoc(ast.right);
            const needParenthesisRightDoc = rightOperandNeedsParenthesis(ast);
            const finalRightDoc = needParenthesisRightDoc ? wrapInParentheses(rightDoc) : rightDoc;
            const operator = `${ast.value}`;
            return group(concat([finalLeftDoc, operator, nest(1, concat([line(), finalRightDoc]))]));
        }
        case "SYMBOL":
            return ast.value;
        case "EMPTY":
            return "";
    }
}
/**
 * Wraps a `Doc` in parentheses (with optional function name).
 */
function wrapInParentheses(doc, functionName = undefined) {
    const docToConcat = ["(", nest(1, concat([line(), doc])), line(), ")"];
    if (functionName) {
        docToConcat.unshift(functionName);
    }
    return group(concat(docToConcat));
}
/**
 * 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":
            if (ast.postfix) {
                const leftOperand = leftOperandNeedsParenthesis(ast)
                    ? `(${astToFormula(ast.operand)})`
                    : astToFormula(ast.operand);
                return leftOperand + ast.value;
            }
            const rightOperand = rightOperandNeedsParenthesis(ast)
                ? `(${astToFormula(ast.operand)})`
                : astToFormula(ast.operand);
            return ast.value + rightOperand;
        case "BIN_OPERATION":
            const leftOperation = leftOperandNeedsParenthesis(ast)
                ? `(${astToFormula(ast.left)})`
                : astToFormula(ast.left);
            const rightOperation = rightOperandNeedsParenthesis(ast)
                ? `(${astToFormula(ast.right)})`
                : astToFormula(ast.right);
            return leftOperation + ast.value + rightOperation;
        default:
            return ast.value;
    }
}
function leftOperandNeedsParenthesis(operationAST) {
    const mainOperator = operationAST.value;
    const leftOperation = "left" in operationAST ? operationAST.left : operationAST.operand;
    const leftOperator = leftOperation.value;
    return (leftOperation.type === "BIN_OPERATION" && OP_PRIORITY[leftOperator] < OP_PRIORITY[mainOperator]);
}
function rightOperandNeedsParenthesis(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];
    if (rightOperation.type !== "BIN_OPERATION") {
        return false;
    }
    if (rightPriority < mainPriority) {
        return true;
    }
    return rightPriority === mainPriority && !ASSOCIATIVE_OPERATORS.includes(mainOperator);
}

const CELL_DELETED_MESSAGE = _t("The cell you are trying to edit has been deleted.");
class CellComposerStore extends AbstractComposerStore {
    canStopEdition() {
        if (this.editionMode === "inactive") {
            return true;
        }
        return this.checkDataValidation();
    }
    stopEdition(direction) {
        const canStopEdition = this.canStopEdition();
        if (canStopEdition) {
            this._stopEdition();
            if (direction) {
                this.model.selection.moveAnchorCell(direction, 1);
            }
            return;
        }
        const editedCell = this.currentEditedCell;
        const cellXc = toXC(editedCell.col, editedCell.row);
        const rule = this.getters.getValidationRuleForCell(editedCell);
        if (!rule) {
            return;
        }
        const evaluator = criterionEvaluatorRegistry.get(rule.criterion.type);
        const errorStr = evaluator.getErrorString(rule.criterion, this.getters, editedCell.sheetId);
        this.notificationStore.raiseError(_t("The data you entered in %s violates the data validation rule set on the cell:\n%s", cellXc, errorStr));
        this.cancelEdition();
    }
    handle(cmd) {
        super.handle(cmd);
        switch (cmd.type) {
            case "SET_FORMATTING":
                this.cancelEdition();
                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 "ACTIVATE_SHEET":
                if (!isFormula(this._currentContent)) {
                    this._cancelEdition();
                    this.resetContent();
                }
                if (cmd.sheetIdFrom !== cmd.sheetIdTo) {
                    const activePosition = this.getters.getActivePosition();
                    const { col, row } = this.getters.getNextVisibleCellPosition({
                        sheetId: cmd.sheetIdTo,
                        col: activePosition.col,
                        row: activePosition.row,
                    });
                    const zone = this.getters.expandZone(cmd.sheetIdTo, positionToZone({ col, row }));
                    this.model.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.editionMode !== "inactive") {
                    this.sheetId = this.getters.getActiveSheetId();
                    this.resetContent();
                    this.cancelEditionAndActivateSheet();
                    this.notificationStore.raiseError(CELL_DELETED_MESSAGE);
                }
                break;
        }
    }
    // ---------------------------------------------------------------------------
    // Getters
    // ---------------------------------------------------------------------------
    get placeholder() {
        const position = this.getters.getActivePosition();
        const spreader = this.model.getters.getArrayFormulaSpreadingOn(position);
        if (!spreader) {
            return undefined;
        }
        return this.getters.getCellText(spreader, { showFormula: true });
    }
    get currentEditedCell() {
        return {
            sheetId: this.sheetId,
            col: this.col,
            row: this.row,
        };
    }
    onColumnsRemoved(cmd) {
        if (cmd.elements.includes(this.col) && this.editionMode !== "inactive") {
            this.cancelEdition();
            this.notificationStore.raiseError(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.editionMode !== "inactive") {
            this.cancelEdition();
            this.notificationStore.raiseError(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;
    }
    confirmEdition(content) {
        if (content) {
            const sheetId = this.getters.getActiveSheetId();
            const cell = this.getters.getEvaluatedCell({ sheetId, col: this.col, row: this.row });
            if (cell.link && !isFormula(content)) {
                content = markdownLink(content, cell.link.url);
            }
            this.addHeadersForSpreadingFormula(content);
            this.model.dispatch("UPDATE_CELL", {
                ...this.currentEditedCell,
                content,
            });
        }
        else {
            this.model.dispatch("UPDATE_CELL", {
                ...this.currentEditedCell,
                content: "",
            });
        }
        this.model.dispatch("AUTOFILL_TABLE_COLUMN", { ...this.currentEditedCell });
        this.setContent("");
    }
    getComposerContent(position, selection) {
        const locale = this.getters.getLocale();
        const cell = this.getters.getCell(position);
        if (cell?.isFormula) {
            const prettifiedContent = this.getPrettifiedFormula(cell);
            // when a formula is prettified (multi lines, indented), adapt the cursor position
            // to take into account line breaks and tabs
            function adjustCursorIndex(targetIndex) {
                let adjustedIndex = 0;
                let originalIndex = 0;
                while (originalIndex < targetIndex) {
                    adjustedIndex++;
                    const char = prettifiedContent[adjustedIndex];
                    if (char !== "\n" && char !== "\t") {
                        originalIndex++;
                    }
                }
                return adjustedIndex;
            }
            let adjustedSelection = selection;
            if (selection) {
                adjustedSelection = {
                    start: adjustCursorIndex(selection.start),
                    end: adjustCursorIndex(selection.end),
                };
            }
            return {
                text: localizeFormula(prettifiedContent, locale),
                adjustedSelection,
            };
        }
        const spreader = this.model.getters.getArrayFormulaSpreadingOn(position);
        if (spreader) {
            return { text: "" };
        }
        const { format, value, type, formattedValue } = this.getters.getEvaluatedCell(position);
        switch (type) {
            case CellValueType.empty:
                return { text: "" };
            case CellValueType.text:
            case CellValueType.error:
                return { text: value };
            case CellValueType.boolean:
                return { text: formattedValue };
            case CellValueType.number:
                if (format && isDateTimeFormat(format)) {
                    if (parseDateTime(formattedValue, locale) !== null) {
                        // formatted string can be parsed again
                        return { text: formattedValue };
                    }
                    // display a simplified and parsable string otherwise
                    const timeFormat = Number.isInteger(value)
                        ? locale.dateFormat
                        : getDateTimeFormat(locale);
                    return { text: formatValue(value, { locale, format: timeFormat }) };
                }
                return { text: this.numberComposerContent(value, format, locale) };
        }
    }
    getPrettifiedFormula(cell) {
        if (cell.compiledFormula.isBadExpression) {
            return cell.content;
        }
        const width = this.editionMode === "inactive"
            ? Infinity // one liner
            : 80;
        return prettify(parseTokens(cell.compiledFormula.tokens), width);
    }
    numberComposerContent(value, format, locale) {
        if (format?.includes("%")) {
            return `${numberToString(value * 100, locale.decimalSeparator)}%`;
        }
        return numberToString(value, locale.decimalSeparator);
    }
    /** Add headers at the end of the sheet so the formula in the composer has enough space to spread */
    addHeadersForSpreadingFormula(content) {
        if (!isFormula(content)) {
            return;
        }
        const evaluated = this.getters.evaluateFormula(this.sheetId, content, {
            sheetId: this.sheetId,
            col: this.col,
            row: this.row,
        });
        if (!isMatrix(evaluated)) {
            return;
        }
        const numberOfRows = this.getters.getNumberRows(this.sheetId);
        const numberOfCols = this.getters.getNumberCols(this.sheetId);
        const missingRows = this.row + evaluated[0].length - numberOfRows;
        const missingCols = this.col + evaluated.length - numberOfCols;
        if (missingCols > 0) {
            this.model.dispatch("ADD_COLUMNS_ROWS", {
                sheetId: this.sheetId,
                sheetName: this.getters.getSheetName(this.sheetId),
                dimension: "COL",
                base: numberOfCols - 1,
                position: "after",
                quantity: missingCols + 20,
            });
        }
        if (missingRows > 0) {
            this.model.dispatch("ADD_COLUMNS_ROWS", {
                sheetId: this.sheetId,
                sheetName: this.getters.getSheetName(this.sheetId),
                dimension: "ROW",
                base: numberOfRows - 1,
                position: "after",
                quantity: missingRows + 50,
            });
        }
    }
    checkDataValidation() {
        const cellPosition = { sheetId: this.sheetId, col: this.col, row: this.row };
        const content = this.getCurrentCanonicalContent();
        const cellValue = isFormula(content)
            ? this.getters.evaluateFormula(this.sheetId, content)
            : parseLiteral(content, this.getters.getLocale());
        if (isMultipleElementMatrix(cellValue)) {
            return true;
        }
        const validationResult = this.getters.getValidationResultForCellValue(toScalar(cellValue), cellPosition);
        if (!validationResult.isValid && validationResult.rule.isBlocking) {
            return false;
        }
        return true;
    }
    evaluateCanonicalFormula(canonicalFormula) {
        return this.getters.evaluateFormulaResult(this.sheetId, canonicalFormula, {
            sheetId: this.sheetId,
            col: this.col,
            row: this.row,
        });
    }
}

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

    display: flex;
    align-items: center;
  }

  div.o-cell-reference {
    position: absolute;
    z-index: ${ComponentsImportance.GridComposer};
    background: ${SELECTION_BORDER_COLOR};
    color: white;
    font-size: 12px;
    line-height: 14px;
    padding: 6px 7px;
    border-radius: 4px;
  }
`;
/**
 * This component is a composer which positions itself on the grid at the anchor cell.
 * It also applies the style of the cell to the composer input.
 */
class GridComposer extends Component {
    static template = "o-spreadsheet-GridComposer";
    static props = {
        gridDims: Object,
        onInputContextMenu: Function,
    };
    static components = { Composer };
    rect = this.defaultRect;
    isEditing = false;
    isCellReferenceVisible = false;
    currentEditedCell = {
        col: 0,
        row: 0,
        sheetId: this.env.model.getters.getActiveSheetId(),
    };
    composerStore;
    composerFocusStore;
    composerInterface;
    get defaultRect() {
        return { x: 0, y: 0, width: 0, height: 0 };
    }
    setup() {
        const composerStore = useStore(CellComposerStore);
        this.composerStore = composerStore;
        this.composerFocusStore = useStore(ComposerFocusStore);
        this.composerInterface = {
            id: "gridComposer",
            get editionMode() {
                return composerStore.editionMode;
            },
            startEdition: this.composerStore.startEdition,
            setCurrentContent: this.composerStore.setCurrentContent,
            stopEdition: this.composerStore.stopEdition,
        };
        this.composerFocusStore.focusComposer(this.composerInterface, { focusMode: "inactive" });
        onWillUpdateProps(() => {
            this.updateComponentPosition();
            this.updateCellReferenceVisibility();
        });
    }
    get shouldDisplayCellReference() {
        return !this.env.isMobile() && this.isCellReferenceVisible;
    }
    get cellReference() {
        const { col, row, sheetId } = this.composerStore.currentEditedCell;
        const prefixSheet = sheetId !== this.env.model.getters.getActiveSheetId();
        return getFullReference(prefixSheet ? this.env.model.getters.getSheetName(sheetId) : undefined, toXC(col, row));
    }
    get cellReferenceStyle() {
        const { x: left, y: top } = this.rect;
        return cssPropertiesToCss({
            left: `${left - COMPOSER_BORDER_WIDTH}px`,
            top: `${top - GRID_CELL_REFERENCE_TOP_OFFSET}px`,
        });
    }
    get focus() {
        return this.composerFocusStore.activeComposer === this.composerInterface
            ? this.composerFocusStore.focusMode
            : "inactive";
    }
    get composerProps() {
        const { width, height } = this.env.model.getters.getSheetViewDimensionWithHeaders();
        // Remove the wrapper border width
        const maxHeight = this.props.gridDims.height - this.rect.y - 2 * COMPOSER_BORDER_WIDTH;
        return {
            rect: { ...this.rect },
            delimitation: {
                width,
                height,
            },
            focus: this.focus,
            isDefaultFocus: true,
            onComposerContentFocused: (selection) => this.composerFocusStore.focusComposer(this.composerInterface, {
                focusMode: "contentFocus",
                selection,
            }),
            onComposerCellFocused: (content) => this.composerFocusStore.focusComposer(this.composerInterface, {
                focusMode: "cellFocus",
                content,
            }),
            onInputContextMenu: this.props.onInputContextMenu,
            composerStore: this.composerStore,
            inputStyle: `max-height: ${maxHeight}px;`,
            inputMode: this.composerStore.editionMode === "inactive" ? "none" : undefined,
        };
    }
    get containerStyle() {
        if (this.composerStore.editionMode === "inactive" || this.env.isMobile()) {
            return `z-index: -1000; opacity: 0;`; // opacity 0 for safari on ios
        }
        const _isFormula = isFormula(this.composerStore.currentContent);
        const cell = this.env.model.getters.getActiveCell();
        const position = this.env.model.getters.getActivePosition();
        const style = this.env.model.getters.getCellComputedStyle(position);
        // position style
        const { x: left, y: top, width, height } = this.rect;
        // color style
        const background = (!_isFormula && style.fillColor) || "#ffffff";
        const color = (!_isFormula && style.textColor) || "#000000";
        // font style
        const fontSize = (!_isFormula && style.fontSize) || 10;
        const fontWeight = !_isFormula && style.bold ? "bold" : undefined;
        const fontStyle = !_isFormula && style.italic ? "italic" : "normal";
        const textDecoration = !_isFormula ? getTextDecoration(style) : "none";
        // align style
        let textAlign = "left";
        if (!_isFormula) {
            textAlign = style.align || cell.defaultAlign;
        }
        const maxHeight = this.props.gridDims.height - this.rect.y;
        const maxWidth = this.props.gridDims.width - this.rect.x;
        /**
         * min-size is on the container, not the composer element, because we want to have the same size as the cell by default,
         * including all the paddings/margins of the composer
         *
         * The +-1 are there to include cell borders in the composer sizing/positioning
         */
        const minHeight = Math.min(height + 1, maxHeight);
        const minWidth = Math.min(width + 1, maxWidth);
        return cssPropertiesToCss({
            left: `${left - 1}px`,
            top: `${top}px`,
            "min-width": `${minWidth}px`,
            "min-height": `${minHeight}px`,
            "max-width": `${maxWidth}px`,
            "max-height": `${maxHeight}px`,
            background,
            color,
            "font-size": `${fontSizeInPixels(fontSize)}px`,
            "font-weight": fontWeight,
            "font-style": fontStyle,
            "text-decoration": textDecoration,
            "text-align": textAlign,
        });
    }
    updateComponentPosition() {
        const isEditing = this.composerFocusStore.activeComposer.editionMode !== "inactive";
        if (!isEditing && this.composerFocusStore.activeComposer !== this.composerInterface) {
            this.composerFocusStore.focusComposer(this.composerInterface, { focusMode: "inactive" });
        }
        let shouldRecomputeRect = !deepEquals(this.currentEditedCell, this.composerStore.currentEditedCell);
        if (this.isEditing !== isEditing) {
            this.isEditing = isEditing;
            if (!isEditing) {
                this.rect = this.defaultRect;
                return;
            }
            this.currentEditedCell = this.composerStore.currentEditedCell;
            shouldRecomputeRect = true;
        }
        if (shouldRecomputeRect) {
            const position = this.env.model.getters.getActivePosition();
            const zone = this.env.model.getters.expandZone(position.sheetId, positionToZone(position));
            this.rect = this.env.model.getters.getVisibleRect(zone);
        }
    }
    updateCellReferenceVisibility() {
        if (this.composerStore.editionMode === "inactive") {
            this.isCellReferenceVisible = false;
            return;
        }
        if (this.isCellReferenceVisible) {
            return;
        }
        const sheetId = this.env.model.getters.getActiveSheetId();
        const zone = positionToZone(this.env.model.getters.getSelection().anchor.cell);
        const rect = this.env.model.getters.getVisibleRect(zone);
        if (!deepEquals(rect, this.rect) || sheetId !== this.composerStore.currentEditedCell.sheetId) {
            this.isCellReferenceVisible = true;
        }
    }
    onFocus() {
        this.composerFocusStore.focusComposer(this.composerInterface, { focusMode: "contentFocus" });
    }
}

function dragFigureForMove({ x: mouseX, y: mouseY }, { x: mouseInitialX, y: mouseInitialY }, initialFigure, { maxX, maxY }, { scrollX: initialScrollX, scrollY: initialScrollY }, { scrollX, scrollY }) {
    const deltaX = mouseX - mouseInitialX + scrollX - initialScrollX;
    const newX = clip(initialFigure.x + deltaX, 0, maxX - initialFigure.width);
    const deltaY = mouseY - mouseInitialY + scrollY - initialScrollY;
    const newY = clip(initialFigure.y + deltaY, 0, maxY - initialFigure.height);
    return { ...initialFigure, x: newX, y: newY };
}
function dragFigureForResize(initialFigure, dirX, dirY, { x: mouseX, y: mouseY }, { x: mouseInitialX, y: mouseInitialY }, keepRatio, minFigSize, { scrollX: initialScrollX, scrollY: initialScrollY }, { scrollX, scrollY }, { maxX, maxY }) {
    let { x, y, width, height } = initialFigure;
    if (keepRatio && dirX !== 0 && dirY !== 0) {
        const deltaX = Math.min(dirX * (mouseInitialX - mouseX + scrollX - initialScrollX), width - minFigSize);
        const deltaY = Math.min(dirY * (mouseInitialY - mouseY + scrollY - initialScrollY), height - minFigSize);
        const fraction = Math.min(deltaX / width, deltaY / height);
        if (dirX < 0) {
            x = x + width * fraction;
        }
        if (dirY < 0) {
            y = y + height * fraction;
        }
        width = width * (1 - fraction);
        height = height * (1 - fraction);
    }
    else {
        const deltaX = Math.max(dirX * (mouseX - mouseInitialX + scrollX - initialScrollX), minFigSize - width);
        const deltaY = Math.max(dirY * (mouseY - mouseInitialY + scrollY - initialScrollY), minFigSize - height);
        width = width + deltaX;
        height = height + deltaY;
        if (dirX < 0) {
            x = x - deltaX;
        }
        if (dirY < 0) {
            y = y - deltaY;
        }
    }
    // Adjusts figure dimensions to ensure it remains within header boundaries and viewport during resizing.
    if (x + scrollX <= 0) {
        width = width + x + scrollX;
        x = -scrollX;
    }
    else if (x + width > maxX) {
        width = maxX - x;
    }
    if (y + scrollY <= 0) {
        height = height + y + scrollY;
        y = -scrollY;
    }
    else if (y + height > maxY) {
        height = maxY - y;
    }
    return { ...initialFigure, x, y, width, height };
}

const SNAP_MARGIN = 5;
/**
 * Try to snap the given figure to other figures when moving the figure, and return the snapped
 * figure and the possible snap lines, if any were found
 */
function snapForMove(getters, figureToSnap, otherFigures) {
    const verticalSnapLine = getSnapLine(getters, figureToSnap, ["hCenter", "right", "left"], otherFigures, ["hCenter", "right", "left"]);
    const horizontalSnapLine = getSnapLine(getters, figureToSnap, ["vCenter", "bottom", "top"], otherFigures, ["vCenter", "bottom", "top"]);
    const { y: viewportY, x: viewportX } = getters.getMainViewportCoordinates();
    const { scrollY, scrollX } = getters.getActiveSheetScrollInfo();
    // If the snap cause the figure to change pane, we need to also apply the scroll as an offset
    if (horizontalSnapLine) {
        figureToSnap.y -= horizontalSnapLine.snapOffset;
        const isBaseFigFrozenY = figureToSnap.y < viewportY;
        const isSnappedFrozenY = figureToSnap.y < viewportY;
        if (isBaseFigFrozenY && !isSnappedFrozenY)
            figureToSnap.y += scrollY;
        else if (!isBaseFigFrozenY && isSnappedFrozenY)
            figureToSnap.y -= scrollY;
    }
    if (verticalSnapLine) {
        figureToSnap.x -= verticalSnapLine.snapOffset;
        const isBaseFigFrozenX = figureToSnap.x < viewportX;
        const isSnappedFrozenX = figureToSnap.x < viewportX;
        if (isBaseFigFrozenX && !isSnappedFrozenX)
            figureToSnap.x += scrollX;
        else if (!isBaseFigFrozenX && isSnappedFrozenX)
            figureToSnap.x -= scrollX;
    }
    return { snappedFigure: figureToSnap, verticalSnapLine, horizontalSnapLine };
}
/**
 * Try to snap the given figure to the other figures when resizing the figure, and return the snapped
 * figure and the possible snap lines, if any were found
 */
function snapForResize(getters, resizeDirX, resizeDirY, figureToSnap, otherFigures) {
    // Vertical snap line
    const verticalSnapLine = getSnapLine(getters, figureToSnap, [resizeDirX === -1 ? "left" : "right"], otherFigures, ["right", "left"]);
    if (verticalSnapLine) {
        if (resizeDirX === 1) {
            figureToSnap.width -= verticalSnapLine.snapOffset;
        }
        else if (resizeDirX === -1) {
            figureToSnap.x -= verticalSnapLine.snapOffset;
            figureToSnap.width += verticalSnapLine.snapOffset;
        }
    }
    // Horizontal snap line
    const horizontalSnapLine = getSnapLine(getters, figureToSnap, [resizeDirY === -1 ? "top" : "bottom"], otherFigures, ["bottom", "top"]);
    if (horizontalSnapLine) {
        if (resizeDirY === 1) {
            figureToSnap.height -= horizontalSnapLine.snapOffset;
        }
        else if (resizeDirY === -1) {
            figureToSnap.y -= horizontalSnapLine.snapOffset;
            figureToSnap.height += horizontalSnapLine.snapOffset;
        }
    }
    figureToSnap.x = Math.round(figureToSnap.x);
    figureToSnap.y = Math.round(figureToSnap.y);
    figureToSnap.height = Math.round(figureToSnap.height);
    figureToSnap.width = Math.round(figureToSnap.width);
    return { snappedFigure: figureToSnap, verticalSnapLine, horizontalSnapLine };
}
/**
 * Get the position of snap axes for the given figure
 *
 * @param figure the figure
 * @param axesTypes the list of axis types to return the positions of
 */
function getVisibleAxes(getters, figure, axesTypes) {
    const axes = axesTypes.map((axisType) => getAxis(getters, figure, false, axisType));
    return axes.filter((axis) => isAxisVisible(getters, figure, axis));
}
function isAxisVisible(getters, figureUI, axis) {
    const { x: mainViewportX, y: mainViewportY } = getters.getMainViewportCoordinates();
    const axisStartEndPositions = [];
    switch (axis.axisType) {
        case "top":
        case "bottom":
        case "vCenter":
            if (figureUI.y < mainViewportY)
                return true;
            axisStartEndPositions.push({ x: figureUI.x, y: axis.position });
            axisStartEndPositions.push({ x: figureUI.x + figureUI.width, y: axis.position });
            break;
        case "left":
        case "right":
        case "hCenter":
            if (figureUI.x < mainViewportX)
                return true;
            axisStartEndPositions.push({ x: axis.position, y: figureUI.y });
            axisStartEndPositions.push({ x: axis.position, y: figureUI.y + figureUI.height });
            break;
    }
    return axisStartEndPositions.some(getters.isPixelPositionVisible);
}
/**
 * Get a snap line for the given figure, if the figure can snap to any other figure
 *
 * @param figureToSnap figure to get the snap line for
 * @param figAxesTypes figure axes of the given figure to be considered to find a snap line
 * @param otherFigures figures to match against the snapped figure to find a snap line
 * @param otherAxesTypes figure axes of the other figures to be considered to find a snap line
 */
function getSnapLine(getters, figureToSnap, figAxesTypes, otherFigures, otherAxesTypes) {
    const axesOfFigure = figAxesTypes.map((axisType) => getAxis(getters, figureToSnap, true, axisType));
    let closestMatch = undefined;
    for (const otherFigure of otherFigures) {
        const axesOfOtherFig = getVisibleAxes(getters, otherFigure, otherAxesTypes);
        for (const axisOfFigure of axesOfFigure) {
            for (const axisOfOtherFig of axesOfOtherFig) {
                if (!canSnap(axisOfFigure.position, axisOfOtherFig.position))
                    continue;
                const snapOffset = axisOfFigure.position - axisOfOtherFig.position;
                if (closestMatch && snapOffset === closestMatch.snapOffset) {
                    closestMatch.matchedFigIds.push(otherFigure.id);
                }
                else if (!closestMatch || Math.abs(snapOffset) <= Math.abs(closestMatch.snapOffset)) {
                    closestMatch = {
                        matchedFigIds: [otherFigure.id],
                        snapOffset,
                        snappedAxisType: axisOfFigure.axisType,
                        position: axisOfOtherFig.position,
                    };
                }
            }
        }
    }
    return closestMatch;
}
/** Check if two axes are close enough to snap */
function canSnap(axisPosition1, axisPosition2) {
    return Math.abs(axisPosition1 - axisPosition2) <= SNAP_MARGIN;
}
function getAxis(getters, figureUI, dnd, axisType) {
    let position = 0;
    const { scrollX, scrollY } = getters.getActiveSheetScrollInfo();
    const { x: viewportX, y: viewportY } = getters.getMainViewportCoordinates();
    const y = !dnd && figureUI.y < viewportY ? figureUI.y + scrollY : figureUI.y;
    const x = !dnd && figureUI.x < viewportX ? figureUI.x + scrollX : figureUI.x;
    switch (axisType) {
        case "top":
            position = y;
            break;
        case "bottom":
            position = y + figureUI.height - FIGURE_BORDER_WIDTH;
            break;
        case "vCenter":
            position = y + Math.floor(figureUI.height / 2) - FIGURE_BORDER_WIDTH;
            break;
        case "left":
            position = x;
            break;
        case "right":
            position = x + figureUI.width - FIGURE_BORDER_WIDTH;
            break;
        case "hCenter":
            position = x + Math.floor(figureUI.width / 2) - FIGURE_BORDER_WIDTH;
            break;
    }
    return { position, axisType: axisType };
}

css /*SCSS*/ `
  .o-figure-snap-line {
    position: relative;
    z-index: ${ComponentsImportance.FigureSnapLine};
    &.vertical {
      width: 0px;
      border-left: 1px dashed black;
    }
    &.horizontal {
      border-top: 1px dashed black;
      height: 0px;
    }
  }
  .o-figure-container {
    -webkit-user-select: none; /* safari */
    user-select: none;
  }
`;
/**
 * 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 Component {
    static template = "o-spreadsheet-FiguresContainer";
    static props = {};
    static components = { FigureComponent };
    dnd = useState({
        draggedFigure: undefined,
        horizontalSnap: undefined,
        verticalSnap: undefined,
        cancelDnd: undefined,
        overlappingCarousel: undefined,
    });
    setup() {
        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();
        });
        onWillUpdateProps(() => {
            const sheetId = this.env.model.getters.getActiveSheetId();
            const draggedFigureId = this.dnd.draggedFigure?.id;
            if (draggedFigureId && !this.env.model.getters.getFigure(sheetId, draggedFigureId)) {
                if (this.dnd.cancelDnd) {
                    this.dnd.cancelDnd();
                }
                this.dnd.draggedFigure = undefined;
                this.dnd.horizontalSnap = undefined;
                this.dnd.verticalSnap = undefined;
                this.dnd.overlappingCarousel = undefined;
                this.dnd.cancelDnd = undefined;
            }
        });
    }
    getVisibleFigures() {
        const visibleFigures = this.env.model.getters.getVisibleFigures();
        if (this.dnd.draggedFigure &&
            !visibleFigures.some((figureUI) => figureUI.id === this.dnd.draggedFigure?.id)) {
            if (this.dnd.draggedFigure) {
                visibleFigures.push(this.dnd.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.draggedFigure) {
            containers.push({
                type: "dnd",
                figures: [this.getDndFigure()],
                style: this.getContainerStyle("dnd"),
                inverseViewportStyle: this.getInverseViewportPositionStyle("dnd"),
            });
        }
        return containers;
    }
    getContainerStyle(container) {
        return this.rectToCss(this.getContainerRect(container));
    }
    rectToCss(rect) {
        return cssPropertiesToCss({
            left: `${rect.x}px`,
            top: `${rect.y}px`,
            width: `${rect.width}px`,
            height: `${rect.height}px`,
        });
    }
    getContainerRect(container) {
        const { width: viewWidth, height: viewHeight } = this.env.model.getters.getSheetViewDimension();
        const { x: viewportX, y: viewportY } = this.env.model.getters.getMainViewportCoordinates();
        const x = ["bottomRight", "topRight"].includes(container) ? viewportX : 0;
        const width = viewWidth - x;
        const y = ["bottomRight", "bottomLeft"].includes(container) ? viewportY : 0;
        const height = viewHeight - y;
        return { x, y, width, height };
    }
    getInverseViewportPositionStyle(container) {
        const { scrollX, scrollY } = this.env.model.getters.getActiveSheetScrollInfo();
        const { x: viewportX, y: viewportY } = this.env.model.getters.getMainViewportCoordinates();
        let left = 0;
        let top = 0;
        if (container === "dnd") {
            left = -scrollX;
            top = -scrollY;
        }
        if (["bottomRight", "topRight"].includes(container)) {
            left = -scrollX - viewportX;
        }
        if (["bottomRight", "bottomLeft"].includes(container)) {
            top = -scrollY - viewportY;
        }
        return cssPropertiesToCss({
            left: `${left}px`,
            top: `${top}px`,
        });
    }
    getFigureContainer(figureUI) {
        const { x: viewportX, y: viewportY } = this.env.model.getters.getMainViewportCoordinates();
        if (figureUI.id === this.dnd.draggedFigure?.id) {
            return "dnd";
        }
        else if (figureUI.x < viewportX && figureUI.y < viewportY) {
            return "topLeft";
        }
        else if (figureUI.x < viewportX) {
            return "bottomLeft";
        }
        else if (figureUI.y < viewportY) {
            return "topRight";
        }
        else {
            return "bottomRight";
        }
    }
    toBottomRightViewport(figureUI) {
        const container = this.getFigureContainer(figureUI);
        const initialScrollPosition = this.env.model.getters.getActiveSheetScrollInfo();
        const bottomRightFigure = { ...figureUI };
        if (["bottomLeft", "topLeft"].includes(container)) {
            bottomRightFigure.x += initialScrollPosition.scrollX;
        }
        if (["topLeft", "topRight"].includes(container)) {
            bottomRightFigure.y += initialScrollPosition.scrollY;
        }
        return bottomRightFigure;
    }
    startDraggingFigure(figureUI, 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", { figureId: figureUI.id });
        if (!selectResult.isSuccessful) {
            return;
        }
        if (this.env.isMobile()) {
            return;
        }
        const sheetId = this.env.model.getters.getActiveSheetId();
        const initialMousePosition = { x: ev.clientX, y: ev.clientY };
        const initialScrollPosition = this.env.model.getters.getActiveSheetScrollInfo();
        const initialFigure = this.toBottomRightViewport(figureUI);
        const maxDimensions = {
            maxX: this.env.model.getters.getColDimensions(sheetId, this.env.model.getters.getNumberCols(sheetId) - 1).end,
            maxY: this.env.model.getters.getRowDimensions(sheetId, this.env.model.getters.getNumberRows(sheetId) - 1).end,
        };
        let hasStartedDnd = false;
        const onMouseMove = (ev) => {
            const getters = this.env.model.getters;
            const currentMousePosition = { x: ev.clientX, y: ev.clientY };
            const offsetX = Math.abs(currentMousePosition.x - initialMousePosition.x);
            const offsetY = Math.abs(currentMousePosition.y - initialMousePosition.y);
            if (!hasStartedDnd && offsetX < DRAG_THRESHOLD && offsetY < DRAG_THRESHOLD) {
                return; // add a small threshold to avoid dnd when just clicking
            }
            hasStartedDnd = true;
            const draggedFigure = dragFigureForMove(currentMousePosition, initialMousePosition, initialFigure, maxDimensions, initialScrollPosition, getters.getActiveSheetScrollInfo());
            const otherFigures = this.getOtherFigures(initialFigure.id);
            const overlappingCarousel = this.getCarouselOverlappingChart(draggedFigure, otherFigures);
            this.dnd.overlappingCarousel = overlappingCarousel;
            if (!overlappingCarousel) {
                const snapResult = snapForMove(getters, draggedFigure, otherFigures);
                this.dnd.draggedFigure = snapResult.snappedFigure;
                this.dnd.horizontalSnap = this.getSnap(snapResult.horizontalSnapLine);
                this.dnd.verticalSnap = this.getSnap(snapResult.verticalSnapLine);
            }
            else {
                this.dnd.draggedFigure = draggedFigure;
                this.dnd.horizontalSnap = undefined;
                this.dnd.verticalSnap = undefined;
            }
        };
        const onMouseUp = (ev) => {
            if (!this.dnd.draggedFigure) {
                return;
            }
            const { col, row, offset } = this.env.model.getters.getPositionAnchorOffset(this.dnd.draggedFigure);
            if (!this.dnd.overlappingCarousel) {
                this.env.model.dispatch("UPDATE_FIGURE", {
                    sheetId,
                    figureId: figureUI.id,
                    offset,
                    col,
                    row,
                });
            }
            else {
                this.env.model.dispatch("ADD_FIGURE_CHART_TO_CAROUSEL", {
                    sheetId,
                    carouselFigureId: this.dnd.overlappingCarousel.id,
                    chartFigureId: figureUI.id,
                });
            }
            this.dnd.draggedFigure = undefined;
            this.dnd.horizontalSnap = undefined;
            this.dnd.verticalSnap = undefined;
            this.dnd.overlappingCarousel = undefined;
        };
        this.dnd.cancelDnd = startDnd(onMouseMove, onMouseUp);
    }
    /**
     * Initialize the resize of a figure with mouse movements
     *
     * @param dirX X direction of the resize. -1 : resize from the left border of the figure, 0 : no resize in X, 1 :
     * resize from the right border of the figure
     * @param dirY Y direction of the resize. -1 : resize from the top border of the figure, 0 : no resize in Y, 1 :
     * resize from the bottom border of the figure
     * @param ev Mouse Event
     */
    startResize(figureUI, dirX, dirY, ev) {
        ev.stopPropagation();
        const initialMousePosition = { x: ev.clientX, y: ev.clientY };
        const initialScrollPosition = this.env.model.getters.getActiveSheetScrollInfo();
        const keepRatio = figureRegistry.get(figureUI.tag).keepRatio || false;
        const minFigSize = figureRegistry.get(figureUI.tag).minFigSize || MIN_FIG_SIZE;
        const sheetId = this.env.model.getters.getActiveSheetId();
        const maxDimensions = {
            maxX: this.env.model.getters.getColDimensions(sheetId, this.env.model.getters.getNumberCols(sheetId) - 1).end,
            maxY: this.env.model.getters.getRowDimensions(sheetId, this.env.model.getters.getNumberRows(sheetId) - 1).end,
        };
        const onMouseMove = (ev) => {
            const currentMousePosition = { x: ev.clientX, y: ev.clientY };
            const draggedFigure = dragFigureForResize(figureUI, dirX, dirY, currentMousePosition, initialMousePosition, keepRatio, minFigSize, initialScrollPosition, this.env.model.getters.getActiveSheetScrollInfo(), maxDimensions);
            const otherFigures = this.getOtherFigures(figureUI.id);
            const snapResult = snapForResize(this.env.model.getters, dirX, dirY, draggedFigure, otherFigures);
            this.dnd.draggedFigure = snapResult.snappedFigure;
            this.dnd.horizontalSnap = this.getSnap(snapResult.horizontalSnapLine);
            this.dnd.verticalSnap = this.getSnap(snapResult.verticalSnapLine);
        };
        const onMouseUp = (ev) => {
            if (!this.dnd.draggedFigure) {
                return;
            }
            const update = this.env.model.getters.getPositionAnchorOffset(this.dnd.draggedFigure);
            if (dirX) {
                update.width = this.dnd.draggedFigure.width;
            }
            if (dirY) {
                update.height = this.dnd.draggedFigure.height;
            }
            this.env.model.dispatch("UPDATE_FIGURE", {
                sheetId: this.env.model.getters.getActiveSheetId(),
                figureId: figureUI.id,
                ...update,
            });
            this.dnd.draggedFigure = undefined;
            this.dnd.horizontalSnap = undefined;
            this.dnd.verticalSnap = undefined;
            this.dnd.overlappingCarousel = undefined;
        };
        this.dnd.cancelDnd = startDnd(onMouseMove, onMouseUp);
    }
    getOtherFigures(figId) {
        return this.getVisibleFigures().filter((f) => f.id !== figId);
    }
    getDndFigure() {
        const figure = this.dnd.draggedFigure;
        if (!figure)
            throw new Error("Dnd figure not found");
        return figure;
    }
    getFigureStyle(figureUI) {
        if (figureUI.id !== this.dnd.draggedFigure?.id)
            return "";
        return cssPropertiesToCss({
            opacity: this.dnd.overlappingCarousel?.id ? "0.6" : "0.9",
            cursor: "grabbing",
        });
    }
    getFigureClass(figureUI) {
        if (figureUI.id !== this.dnd.overlappingCarousel?.id)
            return "";
        return "o-add-to-carousel";
    }
    getSnap(snapLine) {
        if (!snapLine || !this.dnd.draggedFigure)
            return undefined;
        const { scrollX, scrollY } = this.env.model.getters.getActiveSheetScrollInfo();
        const figureVisibleRects = snapLine.matchedFigIds
            .map((id) => this.getVisibleFigures().find((figureUI) => figureUI.id === id))
            .filter(isDefined)
            .map((figureUI) => {
            return {
                x: figureUI.x - scrollX,
                y: figureUI.y - scrollY,
                width: figureUI.width,
                height: figureUI.height,
            };
        })
            .filter(isDefined);
        const containerRect = rectUnion({
            ...this.dnd.draggedFigure,
            x: this.dnd.draggedFigure.x - scrollX,
            y: this.dnd.draggedFigure.y - scrollY,
        }, ...figureVisibleRects);
        return {
            line: snapLine,
            containerStyle: this.rectToCss(containerRect),
            lineStyle: this.getSnapLineStyle(snapLine, containerRect),
        };
    }
    getSnapLineStyle(snapLine, containerRect) {
        if (!snapLine)
            return "";
        const { scrollX, scrollY } = this.env.model.getters.getActiveSheetScrollInfo();
        if (["top", "vCenter", "bottom"].includes(snapLine.snappedAxisType)) {
            return cssPropertiesToCss({
                top: `${snapLine.position - containerRect.y - scrollY}px`,
                left: `0px`,
                width: `100%`,
            });
        }
        else {
            return cssPropertiesToCss({
                top: `0px`,
                left: `${snapLine.position - containerRect.x - scrollX}px`,
                height: `100%`,
            });
        }
    }
    getCarouselOverlappingChart(figureUI, otherFigures) {
        if (figureUI.tag !== "chart") {
            return undefined;
        }
        const figureCenterX = figureUI.x + figureUI.width / 2;
        const figureCenterY = figureUI.y + figureUI.height / 2;
        let bestMatch;
        let smallestDistance = Infinity;
        for (const figure of otherFigures) {
            if (figure.tag !== "carousel") {
                continue;
            }
            const carouselCenterX = figure.x + figure.width / 2;
            const carouselCenterY = figure.y + figure.height / 2;
            const distanceX = Math.abs(figureCenterX - carouselCenterX);
            const distanceY = Math.abs(figureCenterY - carouselCenterY);
            const squaredDistance = distanceX ** 2 + distanceY ** 2;
            if (distanceX <= figureUI.width / 2 &&
                distanceY <= figureUI.height / 2 &&
                squaredDistance < smallestDistance) {
                smallestDistance = squaredDistance;
                bestMatch = figure;
            }
        }
        return bestMatch;
    }
}

css /* scss */ `
  .o-validation {
    border-radius: 4px;
    border-width: 0 0 0 3px;
    border-style: solid;
    gap: 3px;

    .o-icon {
      margin-right: 5px;
      height: 1.2em;
      width: 1.2em;
    }
  }

  .o-validation-warning {
    border-color: ${ALERT_WARNING_BORDER};
    color: ${ALERT_WARNING_TEXT_COLOR};
    background-color: ${ALERT_WARNING_BG};
  }

  .o-validation-error {
    border-color: ${ALERT_DANGER_BORDER};
    color: ${ALERT_DANGER_TEXT_COLOR};
    background-color: ${ALERT_DANGER_BG};
  }

  .o-validation-info {
    border-color: ${ALERT_INFO_BORDER};
    color: ${ALERT_INFO_TEXT_COLOR};
    background-color: ${ALERT_INFO_BG};
  }
`;
class ValidationMessages extends Component {
    static template = "o-spreadsheet-ValidationMessages";
    static props = {
        messages: Array,
        msgType: String,
        singleBox: { type: Boolean, optional: true },
    };
    get divClasses() {
        if (this.props.msgType === "warning") {
            return "o-validation-warning";
        }
        if (this.props.msgType === "info") {
            return "o-validation-info";
        }
        return "o-validation-error";
    }
    get alertBoxes() {
        return this.props.singleBox ? [this.props.messages] : this.props.messages.map((msg) => [msg]);
    }
}

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

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

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

const PAINT_FORMAT_HANDLER_KEYS = [
    "cell",
    "border",
    "table",
    "conditionalFormat",
    "merge",
];
class PaintFormatStore extends SpreadsheetStore {
    mutators = ["activate", "cancel", "pasteFormat"];
    highlightStore = this.get(HighlightStore);
    status = "inactive";
    copiedData;
    constructor(get) {
        super(get);
        this.highlightStore.register(this);
        this.onDispose(() => {
            this.highlightStore.unRegister(this);
        });
    }
    handle(cmd) {
        switch (cmd.type) {
            case "PAINT_FORMAT":
                this.paintFormat(cmd.sheetId, cmd.target);
                break;
        }
    }
    activate(args) {
        this.copiedData = this.copyFormats();
        this.status = args.persistent ? "persistent" : "oneOff";
    }
    cancel() {
        this.status = "inactive";
        this.copiedData = undefined;
    }
    pasteFormat(target) {
        this.model.dispatch("PAINT_FORMAT", { target, sheetId: this.getters.getActiveSheetId() });
    }
    get isActive() {
        return this.status !== "inactive";
    }
    get clipboardHandlers() {
        return PAINT_FORMAT_HANDLER_KEYS.map((handlerName) => {
            const HandlerClass = clipboardHandlersRegistries.cellHandlers.get(handlerName);
            return {
                handlerName,
                handler: new HandlerClass(this.getters, this.model.dispatch),
            };
        });
    }
    copyFormats() {
        const sheetId = this.getters.getActiveSheetId();
        const zones = this.getters.getSelectedZones();
        const copiedData = { zones, sheetId };
        for (const { handlerName, handler } of this.clipboardHandlers) {
            const handlerResult = handler.copy(getClipboardDataPositions(sheetId, zones), false);
            if (handlerResult !== undefined) {
                copiedData[handlerName] = handlerResult;
            }
        }
        return copiedData;
    }
    paintFormat(sheetId, target) {
        if (!this.copiedData) {
            return;
        }
        const options = {
            isCutOperation: false,
            pasteOption: "onlyFormat",
        };
        const { target: pasteTarget, selectedZones } = getPasteTargetFromHandlers(sheetId, target, this.copiedData, this.clipboardHandlers, options);
        applyClipboardHandlersPaste(this.clipboardHandlers, this.copiedData, pasteTarget, options);
        selectPastedZone(this.model.selection, target, selectedZones);
        if (this.status === "oneOff") {
            this.cancel();
        }
    }
    get highlights() {
        const data = this.copiedData;
        if (!data) {
            return [];
        }
        return data.zones.map((zone) => ({
            range: this.model.getters.getRangeFromZone(data.sheetId, zone),
            color: SELECTION_BORDER_COLOR,
            dashed: true,
            sheetId: data.sheetId,
            noFill: true,
            thinLine: true,
            interactive: false,
        }));
    }
}

class HoveredTableStore extends SpreadsheetStore {
    mutators = ["clear", "hover"];
    col;
    row;
    overlayColors = new PositionMap();
    handle(cmd) {
        switch (cmd.type) {
            case "ACTIVATE_SHEET":
                this.clear();
        }
    }
    hover(position) {
        if (!this.getters.isDashboard() || (position.col === this.col && position.row === this.row)) {
            return "noStateChange";
        }
        this.col = position.col;
        this.row = position.row;
        this.computeOverlay();
        return;
    }
    clear() {
        this.col = undefined;
        this.row = undefined;
    }
    computeOverlay() {
        this.overlayColors = new PositionMap();
        const { col, row } = this;
        if (col === undefined || row === undefined) {
            return;
        }
        const sheetId = this.getters.getActiveSheetId();
        const table = this.getters.getTable({ sheetId, col, row });
        if (!table) {
            return;
        }
        const { left, right, top } = table.range.zone;
        const isTableHeader = row < top + table.config.numberOfHeaders;
        const doesTableRowHaveContent = range(left, right + 1).some((col) => {
            return (!this.getters.isColHidden(sheetId, col) &&
                this.getters.getEvaluatedCell({ sheetId, col, row }).formattedValue);
        });
        if (!isTableHeader && doesTableRowHaveContent) {
            for (let col = left; col <= right; col++) {
                this.overlayColors.set({ sheetId, col, row }, TABLE_HOVER_BACKGROUND_COLOR);
            }
        }
    }
}

class HoveredIconStore extends SpreadsheetStore {
    mutators = ["setHoveredIcon"];
    hoveredIcon = undefined;
    setHoveredIcon(icon) {
        this.hoveredIcon = icon;
    }
}

const CURSOR_SVG = /*xml*/ `
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="14" height="16"><path d="M6.5.4c1.3-.8 2.9-.1 3.8 1.4l2.9 5.1c.2.4.9 1.6-.4 2.3l-1.6.9 1.8 3.1c.2.4.1 1-.2 1.2l-1.6 1c-.3.1-.9 0-1.1-.4l-1.8-3.1-1.6 1c-.6.4-1.7 0-2.2-.8L0 4.3"/><path fill="#fff" d="M9.1 2a1.4 1.1 60 0 0-1.7-.6L5.5 2.5l.9 1.6-1 .6-.9-1.6-.6.4 1.8 3.1-1.3.7-1.8-3.1-1 .6 3.8 6.6 6.8-3.98M3.9 8.8 10.82 5l.795 1.4-6.81 3.96"/></svg>
`;
css /* scss */ `
  .o-paint-format-cursor {
    cursor: url("data:image/svg+xml,${encodeURIComponent(CURSOR_SVG)}"), auto;
  }
`;
function useCellHovered(env, gridRef) {
    const delayedHoveredCell = useStore(DelayedHoveredCellStore);
    const hoveredTable = useStore(HoveredTableStore);
    const 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 };
    }
    function getOffsetRelativeToOverlay(ev) {
        const gridRect = getBoundingRectAsPOJO(gridRef.el);
        return {
            x: ev.clientX - gridRect.x,
            y: ev.clientY - gridRect.y,
        };
    }
    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 (isChildEvent(gridRef.el, e)) {
            ({ x, y } = getOffsetRelativeToOverlay(e));
            lastMoved = Date.now();
            hoveredTable.hover(getPosition());
        }
    }
    function recompute() {
        const { col, row } = getPosition();
        if (col !== hoveredPosition.col || row !== hoveredPosition.row) {
            setPosition(undefined, undefined);
        }
    }
    function onMouseLeave(e) {
        const { x, y } = getOffsetRelativeToOverlay(e);
        const gridRect = getBoundingRectAsPOJO(gridRef.el);
        if (y < 0 || y > gridRect.height || x < 0 || x > gridRect.width) {
            return updateMousePosition(e);
        }
        else {
            return pause();
        }
    }
    useRefListener(gridRef, "pointermove", (ev) => !env.isMobile() && updateMousePosition(ev));
    useRefListener(gridRef, "mouseleave", onMouseLeave);
    useRefListener(gridRef, "mouseenter", resume);
    useRefListener(gridRef, "pointerdown", recompute);
    useRefListener(gridRef, "pointerdown", (ev) => env.isMobile() && updateMousePosition(ev));
    useExternalListener(window, "click", handleGlobalClick);
    function handleGlobalClick(e) {
        const target = e.target;
        const grid = gridRef.el;
        if (!grid.contains(target)) {
            setPosition(undefined, undefined);
        }
    }
    function setPosition(col, row) {
        if (col !== hoveredPosition.col || row !== hoveredPosition.row) {
            hoveredPosition.col = col;
            hoveredPosition.row = row;
            delayedHoveredCell.hover({ col, row });
        }
    }
    return hoveredPosition;
}
class GridOverlay extends Component {
    static template = "o-spreadsheet-GridOverlay";
    static props = {
        onCellDoubleClicked: { type: Function, optional: true },
        onCellClicked: { type: Function, optional: true },
        onCellRightClicked: { type: Function, optional: true },
        onGridResized: { type: Function, optional: true },
        onGridMoved: Function,
        gridOverlayDimensions: String,
        slots: { type: Object, optional: true },
    };
    static components = {
        FiguresContainer,
        GridAddRowsFooter,
    };
    static defaultProps = {
        onCellDoubleClicked: () => { },
        onCellClicked: () => { },
        onCellRightClicked: () => { },
        onGridResized: () => { },
    };
    gridOverlay = useRef("gridOverlay");
    cellPopovers;
    paintFormatStore;
    hoveredIconStore;
    setup() {
        useCellHovered(this.env, this.gridOverlay);
        const resizeObserver = new ResizeObserver(() => {
            this.props.onGridResized();
        });
        onMounted(() => {
            resizeObserver.observe(this.gridOverlayEl);
        });
        onWillUnmount(() => {
            resizeObserver.disconnect();
        });
        this.cellPopovers = useStore(CellPopoverStore);
        this.paintFormatStore = useStore(PaintFormatStore);
        this.hoveredIconStore = useStore(HoveredIconStore);
    }
    get gridOverlayEl() {
        if (!this.gridOverlay.el) {
            throw new Error("GridOverlay el is not defined.");
        }
        return this.gridOverlay.el;
    }
    get style() {
        return (this.props.gridOverlayDimensions +
            cssPropertiesToCss({ cursor: this.hoveredIconStore.hoveredIcon ? "pointer" : "default" }));
    }
    get isPaintingFormat() {
        return this.paintFormatStore.isActive;
    }
    onPointerMove(ev) {
        if (this.env.isMobile()) {
            return;
        }
        const icon = this.getInteractiveIconAtEvent(ev);
        const hoveredIcon = icon?.type ? { id: icon.type, position: icon.position } : undefined;
        if (!deepEquals(hoveredIcon, this.hoveredIconStore.hoveredIcon)) {
            this.hoveredIconStore.setHoveredIcon(hoveredIcon);
        }
    }
    onPointerDown(ev) {
        if (ev.button > 0 || this.env.isMobile()) {
            // not main button, probably a context menu
            return;
        }
        this.onCellClicked(ev);
    }
    onClick(ev) {
        if (ev.button > 0 || !this.env.isMobile()) {
            // not main button, probably a context menu
            return;
        }
        this.onCellClicked(ev);
    }
    onCellClicked(ev) {
        const openedPopover = this.cellPopovers.persistentCellPopover;
        const [col, row] = this.getCartesianCoordinates(ev);
        const clickedIcon = this.getInteractiveIconAtEvent(ev);
        if (clickedIcon) {
            this.env.model.selection.getBackToDefault();
        }
        this.props.onCellClicked(col, row, {
            expandZone: ev.shiftKey,
            addZone: isCtrlKey(ev),
        }, ev);
        if (clickedIcon?.onClick) {
            clickedIcon.onClick(clickedIcon.position, this.env);
        }
        if (ev.target === this.gridOverlay.el &&
            this.cellPopovers.isOpen &&
            deepEquals(openedPopover, this.cellPopovers.persistentCellPopover)) {
            // Only close the popover if props.click/icon.click didn't open a new one
            this.cellPopovers.close();
            return;
        }
    }
    onDoubleClick(ev) {
        if (this.getInteractiveIconAtEvent(ev)) {
            return;
        }
        const [col, row] = this.getCartesianCoordinates(ev);
        this.props.onCellDoubleClicked(col, row);
    }
    onContextMenu(ev) {
        const [col, row] = this.getCartesianCoordinates(ev);
        this.props.onCellRightClicked(col, row, { x: ev.clientX, y: ev.clientY });
    }
    getCartesianCoordinates(ev) {
        const gridOverLayRect = getRefBoundingRect(this.gridOverlay);
        const x = ev.clientX - gridOverLayRect.x;
        const y = ev.clientY - gridOverLayRect.y;
        const colIndex = this.env.model.getters.getColIndex(x);
        const rowIndex = this.env.model.getters.getRowIndex(y);
        return [colIndex, rowIndex];
    }
    getInteractiveIconAtEvent(ev) {
        const gridOverLayRect = getRefBoundingRect(this.gridOverlay);
        const gridOffset = this.env.model.getters.getGridOffset();
        const x = ev.clientX - gridOverLayRect.x + gridOffset.x;
        const y = ev.clientY - gridOverLayRect.y + gridOffset.y;
        const [col, row] = this.getCartesianCoordinates(ev);
        const sheetId = this.env.model.getters.getActiveSheetId();
        let position = { col, row, sheetId };
        const merge = this.env.model.getters.getMerge(position);
        if (merge) {
            position = { col: merge.left, row: merge.top, sheetId };
        }
        const icons = this.env.model.getters.getCellIcons(position);
        const icon = icons.find((icon) => {
            const merge = this.env.model.getters.getMerge(position);
            const zone = merge || positionToZone(position);
            const cellRect = this.env.model.getters.getRect(zone);
            return isPointInsideRect(x, y, this.env.model.getters.getCellIconRect(icon, cellRect));
        });
        return icon?.onClick ? icon : undefined;
    }
}

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

class UnhideRowHeaders extends Component {
    static template = "o-spreadsheet-UnhideRowHeaders";
    static props = {
        headersGroups: Array,
        headerRange: Object,
        offset: { type: Number, optional: true },
    };
    static defaultProps = { offset: 0 };
    get sheetId() {
        return this.env.model.getters.getActiveSheetId();
    }
    getUnhidePreviousButtonStyle(hiddenIndex) {
        const rect = this.env.model.getters.getRect(positionToZone({ col: 0, row: hiddenIndex }));
        const y = rect.y + rect.height - HEADER_HEIGHT;
        return cssPropertiesToCss({ top: y - this.props.offset + "px", "margin-right": "1px" });
    }
    getUnhideNextButtonStyle(hiddenIndex) {
        const rect = this.env.model.getters.getRect(positionToZone({ col: 0, row: hiddenIndex }));
        const y = rect.y - HEADER_HEIGHT;
        return cssPropertiesToCss({ top: y - this.props.offset + "px", "margin-right": "1px" });
    }
    unhide(hiddenElements) {
        this.env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
            sheetId: this.sheetId,
            dimension: "ROW",
            elements: hiddenElements,
        });
    }
    isVisible(header) {
        return header >= this.props.headerRange.start && header <= this.props.headerRange.end;
    }
}
class UnhideColumnHeaders extends Component {
    static template = "o-spreadsheet-UnhideColumnHeaders";
    static props = {
        headersGroups: Array,
        headerRange: Object,
        offset: { type: Number, optional: true },
    };
    static defaultProps = { offset: 0 };
    get sheetId() {
        return this.env.model.getters.getActiveSheetId();
    }
    getUnhidePreviousButtonStyle(hiddenIndex) {
        const rect = this.env.model.getters.getRect(positionToZone({ col: hiddenIndex, row: 0 }));
        const x = rect.x + rect.width - HEADER_WIDTH;
        return cssPropertiesToCss({ left: x - this.props.offset + "px" });
    }
    getUnhideNextButtonStyle(hiddenIndex) {
        const rect = this.env.model.getters.getRect(positionToZone({ col: hiddenIndex, row: 0 }));
        const x = rect.x - HEADER_WIDTH;
        return cssPropertiesToCss({ left: x - this.props.offset + "px" });
    }
    unhide(hiddenElements) {
        this.env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
            sheetId: this.sheetId,
            dimension: "COL",
            elements: hiddenElements,
        });
    }
    isVisible(header) {
        return header >= this.props.headerRange.start && header <= this.props.headerRange.end;
    }
}

class AbstractResizer extends Component {
    static props = {
        onOpenContextMenu: Function,
    };
    composerFocusStore;
    PADDING = 0;
    MAX_SIZE_MARGIN = 0;
    MIN_ELEMENT_SIZE = 0;
    lastSelectedElementIndex = null;
    state = 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",
    });
    dragNDropGrid = useDragAndDropBeyondTheViewport(this.env);
    setup() {
        this.composerFocusStore = useStore(ComposerFocusStore);
    }
    _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.env.isMobile() ||
            this.env.model.getters.isReadonly() ||
            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);
    }
    onClick(ev) {
        if (!this.env.isMobile()) {
            return;
        }
        if (ev.button > 0) {
            // not main button, probably a context menu
            return;
        }
        const index = this._getElementIndex(this._getEvOffset(ev));
        this._selectElement(index, false);
    }
    select(ev) {
        if (this.env.isMobile()) {
            return;
        }
        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.env.model.getters.isReadonly()) {
            this._selectElement(index, false);
            return;
        }
        if (this.state.waitingForMove) {
            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.composerFocusStore.activeComposer.editionMode === "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) => {
            const 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);
        };
        this.dragNDropGrid.start(ev, mouseMoveMovement, mouseUpMovement);
    }
    startSelection(ev, index) {
        if (this.env.isMobile()) {
            return;
        }
        this.state.isSelecting = true;
        if (ev.shiftKey) {
            this._increaseSelection(index);
        }
        else {
            this._selectElement(index, isCtrlKey(ev));
        }
        this.lastSelectedElementIndex = index;
        const mouseMoveSelect = (col, row) => {
            const newIndex = this._getType() === "COL" ? col : row;
            if (newIndex !== this.lastSelectedElementIndex && newIndex !== -1) {
                this._increaseSelection(newIndex);
                this.lastSelectedElementIndex = newIndex;
            }
        };
        const mouseUpSelect = () => {
            this.state.isSelecting = false;
            this.lastSelectedElementIndex = null;
            this._computeGrabDisplay(ev);
        };
        this.dragNDropGrid.start(ev, 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;
    width: calc(100% - ${HEADER_WIDTH + SCROLLBAR_WIDTH}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 {
      color: ${ICONS_COLOR};
    }
    .o-unhide:hover {
      z-index: ${ComponentsImportance.Grid + 1};
      background-color: lightgrey;
    }
  }
`;
class ColResizer extends AbstractResizer {
    static props = {
        onOpenContextMenu: Function,
    };
    static template = "o-spreadsheet-ColResizer";
    static components = { UnhideColumnHeaders };
    colResizerRef;
    setup() {
        super.setup();
        this.colResizerRef = 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,
            sheetName: this.env.model.getters.getActiveSheetName(),
            dimension: "COL",
            base: this.state.base,
            elements,
            position: this.state.position,
        });
        if (!result.isSuccessful && result.reasons.includes("WillRemoveExistingMerge" /* CommandResult.WillRemoveExistingMerge */)) {
            this.env.raiseError(MergeErrorMessage);
        }
    }
    _selectElement(index, addDistinctHeader) {
        this.env.model.selection.selectColumn(index, addDistinctHeader ? "newAnchor" : "overrideSelection");
    }
    _increaseSelection(index) {
        this.env.model.selection.selectColumn(index, "updateAnchor");
    }
    _fitElementSize(index) {
        const cols = this.env.model.getters.getActiveCols();
        this.env.model.dispatch("AUTORESIZE_COLUMNS", {
            sheetId: this.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",
        });
    }
    get mainUnhideHeadersProps() {
        const { left, right } = this.env.model.getters.getActiveMainViewport();
        const { xSplit } = this.env.model.getters.getPaneDivisions(this.sheetId);
        const hiddenGroups = this.env.model.getters.getHiddenColsGroups(this.sheetId);
        const index = hiddenGroups.findIndex((group) => group[0] >= xSplit - 1);
        return {
            headersGroups: hiddenGroups.slice(index),
            offset: this.env.model.getters.getMainViewportCoordinates().x,
            headerRange: { start: left, end: right },
        };
    }
    get frozenUnhideHeadersProps() {
        const { xSplit } = this.env.model.getters.getPaneDivisions(this.sheetId);
        const hiddenGroups = this.env.model.getters.getHiddenColsGroups(this.sheetId);
        const index = hiddenGroups.findIndex((group) => group[0] >= xSplit - 1);
        return {
            headersGroups: hiddenGroups.slice(0, index + 1),
            headerRange: { start: 0, end: xSplit - 1 },
        };
    }
    get frozenContainerStyle() {
        return cssPropertiesToCss({
            width: this.env.model.getters.getMainViewportCoordinates().x + "px",
        });
    }
    get hasFrozenPane() {
        return this.env.model.getters.getPaneDivisions(this.sheetId).xSplit > 0;
    }
}
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 {
      color: ${ICONS_COLOR};
    }
    .o-unhide:hover {
      z-index: ${ComponentsImportance.Grid + 1};
      background-color: lightgrey;
    }
  }
`;
class RowResizer extends AbstractResizer {
    static props = {
        onOpenContextMenu: Function,
    };
    static template = "o-spreadsheet-RowResizer";
    static components = { UnhideRowHeaders };
    setup() {
        super.setup();
        this.rowResizerRef = useRef("rowResizer");
        this.PADDING = 5;
        this.MAX_SIZE_MARGIN = 60;
        this.MIN_ELEMENT_SIZE = MIN_ROW_HEIGHT;
    }
    rowResizerRef;
    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,
            sheetName: this.env.model.getters.getActiveSheetName(),
            dimension: "ROW",
            base: this.state.base,
            elements,
            position: this.state.position,
        });
        if (!result.isSuccessful) {
            if (result.reasons.includes("WillRemoveExistingMerge" /* CommandResult.WillRemoveExistingMerge */)) {
                this.env.raiseError(MergeErrorMessage);
            }
            else if (result.reasons.includes("CannotMoveTableHeader" /* CommandResult.CannotMoveTableHeader */)) {
                this.env.raiseError(TableHeaderMoveErrorMessage);
            }
        }
    }
    _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;
    }
    get mainUnhideHeadersProps() {
        const { top, bottom } = this.env.model.getters.getActiveMainViewport();
        const { ySplit } = this.env.model.getters.getPaneDivisions(this.sheetId);
        const hiddenGroups = this.env.model.getters.getHiddenRowsGroups(this.sheetId);
        const index = hiddenGroups.findIndex((group) => group[0] >= ySplit - 1);
        return {
            headersGroups: hiddenGroups.slice(index),
            offset: this.env.model.getters.getMainViewportCoordinates().y,
            headerRange: { start: top, end: bottom },
        };
    }
    get frozenUnhideHeadersProps() {
        const { ySplit } = this.env.model.getters.getPaneDivisions(this.sheetId);
        const hiddenGroups = this.env.model.getters.getHiddenRowsGroups(this.sheetId);
        const index = hiddenGroups.findIndex((group) => group[0] >= ySplit - 1);
        return {
            headersGroups: hiddenGroups.slice(0, index + 1),
            headerRange: { start: 0, end: ySplit - 1 },
        };
    }
    get frozenContainerStyle() {
        return cssPropertiesToCss({
            height: this.env.model.getters.getMainViewportCoordinates().y + "px",
        });
    }
    get hasFrozenPane() {
        return this.env.model.getters.getPaneDivisions(this.sheetId).ySplit > 0;
    }
}
css /* scss */ `
  .o-overlay {
    .all {
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      width: ${HEADER_WIDTH}px;
      height: ${HEADER_HEIGHT}px;
    }
  }
`;
class HeadersOverlay extends Component {
    static props = {
        onOpenContextMenu: Function,
    };
    static template = "o-spreadsheet-HeadersOverlay";
    static components = { ColResizer, RowResizer };
    selectAll() {
        this.env.model.selection.selectAll();
    }
}

const CELL_ANIMATION_DURATION = 200;
class GridRenderer extends SpreadsheetStore {
    fingerprints;
    hoveredTables;
    hoveredIcon;
    lastRenderBoxes = new Map();
    preventNewAnimationsInNextFrame = false;
    zonesWithPreventedAnimationsInNextFrame = [];
    animations = new Map();
    constructor(get) {
        super(get);
        this.getters = get(ModelStore).getters;
        this.fingerprints = get(FormulaFingerprintStore);
        this.hoveredTables = get(HoveredTableStore);
        this.hoveredIcon = get(HoveredIconStore);
    }
    handle(cmd) {
        switch (cmd.type) {
            case "START":
            case "ACTIVATE_SHEET":
            case "ADD_COLUMNS_ROWS":
            case "REMOVE_COLUMNS_ROWS":
                this.animations.clear();
                this.preventNewAnimationsInNextFrame = true;
                break;
            case "RESIZE_COLUMNS_ROWS":
                this.preventNewAnimationsInNextFrame = true;
                break;
            case "REDO":
                this.zonesWithPreventedAnimationsInNextFrame = [];
                break;
            case "UNDO":
                for (const command of cmd.commands) {
                    if (command.type === "ADD_COLUMNS_ROWS" ||
                        command.type === "REMOVE_COLUMNS_ROWS" ||
                        command.type === "RESIZE_COLUMNS_ROWS") {
                        this.animations.clear();
                        this.preventNewAnimationsInNextFrame = true;
                        break;
                    }
                }
                break;
            case "PASTE":
                this.zonesWithPreventedAnimationsInNextFrame.push(...this.getters.getSelectedZones());
                break;
            case "UPDATE_CELL":
                const zones = this.getters.getCommandZones(cmd);
                this.zonesWithPreventedAnimationsInNextFrame.push(...zones);
                break;
        }
    }
    finalize() {
        this.zonesWithPreventedAnimationsInNextFrame = recomputeZones(this.zonesWithPreventedAnimationsInNextFrame);
    }
    get renderingLayers() {
        return ["Background", "Headers"];
    }
    // ---------------------------------------------------------------------------
    // Grid rendering
    // ---------------------------------------------------------------------------
    drawLayer(renderingContext, layer, timeStamp) {
        switch (layer) {
            case "Background":
                this.drawGlobalBackground(renderingContext);
                const oldBoxes = this.lastRenderBoxes;
                this.lastRenderBoxes = new Map();
                for (const { zone, rect } of this.getters.getAllActiveViewportsZonesAndRect()) {
                    const { ctx } = renderingContext;
                    ctx.save();
                    ctx.beginPath();
                    ctx.rect(rect.x, rect.y, rect.width, rect.height);
                    ctx.clip();
                    const boxesWithoutAnimations = this.getGridBoxes(zone);
                    const boxes = this.getBoxesWithAnimations(boxesWithoutAnimations, oldBoxes, timeStamp);
                    this.drawBackground(renderingContext, boxes);
                    this.drawOverflowingCellBackground(renderingContext, boxes);
                    this.drawCellBackground(renderingContext, boxes);
                    this.drawBorders(renderingContext, boxes);
                    this.drawTexts(renderingContext, boxes);
                    this.drawIcon(renderingContext, boxes);
                    ctx.restore();
                }
                this.drawFrozenPanes(renderingContext);
                this.preventNewAnimationsInNextFrame = false;
                this.zonesWithPreventedAnimationsInNextFrame = [];
                break;
            case "Headers":
                if (!this.getters.isDashboard()) {
                    this.drawHeaders(renderingContext);
                    this.drawFrozenPanesHeaders(renderingContext);
                }
                break;
        }
    }
    drawGlobalBackground(renderingContext) {
        const { ctx } = renderingContext;
        const { width, height } = this.getters.getSheetViewDimensionWithHeaders();
        // white background
        ctx.fillStyle = "#ffffff";
        ctx.fillRect(0, 0, width + CANVAS_SHIFT, height + CANVAS_SHIFT);
    }
    drawBackground(renderingContext, boxes) {
        const { ctx, thinLineWidth } = renderingContext;
        const areGridLinesVisible = !this.getters.isDashboard() &&
            this.getters.getGridLinesVisibility(this.getters.getActiveSheetId());
        const inset = areGridLinesVisible ? 0.1 * thinLineWidth : 0;
        if (areGridLinesVisible) {
            for (const box of boxes) {
                if (box.skipCellGridLines)
                    continue;
                ctx.strokeStyle = CELL_BORDER_COLOR;
                ctx.lineWidth = thinLineWidth;
                ctx.strokeRect(box.x + inset, box.y + inset, box.width - 2 * inset, box.height - 2 * inset);
            }
        }
    }
    drawCellBackground(renderingContext, boxes) {
        const { ctx } = renderingContext;
        for (const box of boxes) {
            const style = box.style;
            if (style.fillColor && style.fillColor !== "#ffffff") {
                ctx.fillStyle = style.fillColor || "#ffffff";
                ctx.fillRect(box.x, box.y, box.width, box.height);
            }
            if (box.dataBarFill) {
                ctx.fillStyle = box.dataBarFill.color;
                const percentage = box.dataBarFill.percentage;
                const width = box.width * (percentage / 100);
                ctx.fillRect(box.x, box.y, width, box.height);
            }
            if (box?.chip) {
                ctx.save();
                ctx.beginPath();
                ctx.rect(box.x, box.y, box.width, box.height);
                ctx.clip();
                const chip = box.chip;
                ctx.fillStyle = chip.color;
                const radius = 10;
                ctx.beginPath();
                ctx.roundRect(chip.x, chip.y, chip.width, chip.height, radius);
                ctx.fill();
                ctx.restore();
            }
            if (box.overlayColor) {
                ctx.fillStyle = box.overlayColor;
                ctx.fillRect(box.x, box.y, box.width, box.height);
            }
            if (box.isError) {
                ctx.fillStyle = "red";
                ctx.beginPath();
                ctx.moveTo(box.x + box.width - 5, box.y);
                ctx.lineTo(box.x + box.width, box.y);
                ctx.lineTo(box.x + box.width, box.y + 5);
                ctx.fill();
            }
        }
    }
    drawOverflowingCellBackground(renderingContext, boxes) {
        const { ctx, thinLineWidth } = renderingContext;
        for (const box of boxes) {
            if (box.content && box.isOverflow) {
                const align = box.content.align || "left";
                let x;
                let width;
                const y = box.y + thinLineWidth / 2;
                const height = box.height - thinLineWidth;
                const clipWidth = Math.min(box.clipRect?.width || Infinity, box.content.width);
                if (align === "left") {
                    x = box.x + thinLineWidth / 2;
                    width = clipWidth - 2 * thinLineWidth;
                }
                else if (align === "right") {
                    x = box.x + box.width - thinLineWidth / 2;
                    width = -clipWidth + 2 * thinLineWidth;
                }
                else {
                    x =
                        (box.clipRect?.x || box.x + box.width / 2 - box.content.width / 2) + thinLineWidth / 2;
                    width = clipWidth - 2 * thinLineWidth;
                }
                ctx.fillStyle = "#ffffff";
                ctx.fillRect(x, y, width, height);
            }
        }
    }
    drawBorders(renderingContext, boxes) {
        const { ctx } = renderingContext;
        for (const box of boxes) {
            const border = box.border;
            if (border) {
                const { x, y, width, height } = box;
                if (border.left) {
                    drawBorder(border.left, x, y, x, y + height);
                }
                if (border.top) {
                    drawBorder(border.top, x, y, x + width, y);
                }
                if (border.right) {
                    drawBorder(border.right, x + width, y, x + width, y + height);
                }
                if (border.bottom) {
                    drawBorder(border.bottom, x, y + height, x + width, y + height);
                }
            }
        }
        /**
         * Following https://usefulangle.com/post/17/html5-canvas-drawing-1px-crisp-straight-lines,
         * we need to make sure that a "single" pixel line is drawn on a "half" pixel coordinate,
         * while a "double" pixel line is drawn on a "full" pixel coordinate. As, in the rendering
         * process, we always had 0.5 before rendering line (to make sure it is drawn on a "half"
         * pixel), we need to correct this behavior for the "medium" and the "dotted" styles, as
         * they are drawing a two pixels width line.
         * We also adapt here the coordinates of the line to make sure corner are correctly drawn,
         * avoiding a "round corners" effect. This is done by subtracting 1 pixel to the origin of
         * each line and adding 1 pixel to the end of each line (depending on the direction of the
         * line).
         */
        function drawBorder({ color, style, opacity }, x1, y1, x2, y2) {
            ctx.globalAlpha = opacity ?? 1;
            ctx.strokeStyle = color;
            switch (style) {
                case "medium":
                    ctx.lineWidth = 2;
                    x1 += y1 === y2 ? -0.5 : 0.5;
                    x2 += y1 === y2 ? 1.5 : 0.5;
                    y1 += x1 === x2 ? -0.5 : 0.5;
                    y2 += x1 === x2 ? 1.5 : 0.5;
                    break;
                case "thick":
                    ctx.lineWidth = 3;
                    if (y1 === y2) {
                        x1--;
                        x2++;
                    }
                    if (x1 === x2) {
                        y1--;
                        y2++;
                    }
                    break;
                case "dashed":
                    ctx.lineWidth = 1;
                    ctx.setLineDash([1, 3]);
                    break;
                case "dotted":
                    ctx.lineWidth = 1;
                    if (y1 === y2) {
                        x1 += 0.5;
                        x2 += 0.5;
                    }
                    if (x1 === x2) {
                        y1 += 0.5;
                        y2 += 0.5;
                    }
                    ctx.setLineDash([1, 1]);
                    break;
                case "thin":
                default:
                    ctx.lineWidth = 1;
                    break;
            }
            ctx.beginPath();
            ctx.moveTo(x1, y1);
            ctx.lineTo(x2, y2);
            ctx.stroke();
            ctx.lineWidth = 1;
            ctx.setLineDash([]);
            ctx.globalAlpha = 1;
        }
    }
    drawTexts(renderingContext, boxes) {
        const { ctx } = renderingContext;
        ctx.textBaseline = "top";
        let currentFont;
        for (const box of boxes) {
            if (box.content) {
                ctx.globalAlpha = box.textOpacity ?? 1;
                const style = box.style || {};
                const align = box.content.align || "left";
                // compute font and textColor
                const font = computeTextFont(style);
                if (font !== currentFont) {
                    currentFont = font;
                    ctx.font = font;
                }
                ctx.fillStyle = style.textColor || "#000";
                // horizontal align text direction
                ctx.textAlign = align;
                // clip rect if needed
                if (box.clipRect) {
                    ctx.save();
                    ctx.beginPath();
                    const { x, y, width, height } = box.clipRect;
                    ctx.rect(x, y, width, height);
                    ctx.clip();
                }
                const x = box.content.x;
                let y = box.content.y;
                // use the horizontal and the vertical start points to:
                // fill text / fill strikethrough / fill underline
                for (const brokenLine of box.content.textLines) {
                    drawDecoratedText(ctx, brokenLine, { x, y }, style.underline, style.strikethrough);
                    y += MIN_CELL_TEXT_MARGIN + box.content.fontSizePx;
                }
                if (box.clipRect) {
                    ctx.restore();
                }
                ctx.globalAlpha = 1;
            }
        }
    }
    drawIcon(renderingContext, boxes) {
        const { ctx } = renderingContext;
        for (const box of boxes) {
            for (const icon of Object.values(box.icons)) {
                if (!icon) {
                    continue;
                }
                const isHovered = deepEquals({ id: icon.type, position: icon.position }, this.hoveredIcon.hoveredIcon);
                const svg = isHovered ? icon.hoverSvg || icon.svg : icon.svg;
                if (!svg) {
                    continue;
                }
                ctx.save();
                ctx.globalAlpha = icon.opacity ?? 1;
                ctx.beginPath();
                const clipRect = icon.clipRect || box;
                ctx.rect(clipRect.x, clipRect.y, clipRect.width, clipRect.height);
                ctx.clip();
                const iconSize = icon.size;
                const { x, y } = this.getters.getCellIconRect(icon, box);
                ctx.translate(x, y);
                ctx.scale(iconSize / svg.width, iconSize / svg.height);
                for (const path of svg.paths) {
                    ctx.fillStyle = path.fillColor;
                    ctx.fill(getPath2D(path.path));
                }
                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 isColActive = activeCols.has(col);
            const isColSelected = selectedCols.has(col);
            if (isColActive) {
                ctx.fillStyle = BACKGROUND_HEADER_ACTIVE_COLOR;
            }
            else if (isColSelected) {
                ctx.fillStyle = BACKGROUND_HEADER_SELECTED_COLOR;
            }
            else {
                ctx.fillStyle = 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 isRowActive = activeRows.has(row);
            const isRowSelected = selectedRows.has(row);
            if (isRowActive) {
                ctx.fillStyle = BACKGROUND_HEADER_ACTIVE_COLOR;
            }
            else if (isRowSelected) {
                ctx.fillStyle = BACKGROUND_HEADER_SELECTED_COLOR;
            }
            else {
                ctx.fillStyle = 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();
        // column text + separator
        for (const col of visibleCols) {
            const colName = numberToLetters(col);
            ctx.fillStyle = activeCols.has(col) ? "#fff" : TEXT_HEADER_COLOR;
            const zone = { left: col, right: col, top: top, bottom: top };
            const { x: colStart, width: colSize } = this.getters.getRect(zone);
            const { x, width } = this.getters.getVisibleRect(zone);
            ctx.save();
            ctx.beginPath();
            ctx.rect(x, 0, width, HEADER_HEIGHT);
            ctx.clip();
            ctx.fillText(colName, colStart + colSize / 2, HEADER_HEIGHT / 2);
            ctx.restore();
            ctx.beginPath();
            ctx.moveTo(colStart + colSize, 0);
            ctx.lineTo(colStart + colSize, HEADER_HEIGHT);
            ctx.stroke();
        }
        // row text + separator
        for (const row of visibleRows) {
            ctx.fillStyle = activeRows.has(row) ? "#fff" : TEXT_HEADER_COLOR;
            const zone = { top: row, bottom: row, left: left, right: left };
            const { y: rowStart, height: rowSize } = this.getters.getRect(zone);
            const { y, height } = this.getters.getVisibleRect(zone);
            ctx.save();
            ctx.beginPath();
            ctx.rect(0, y, HEADER_WIDTH, height);
            ctx.clip();
            ctx.fillText(String(row + 1), HEADER_WIDTH / 2, rowStart + rowSize / 2);
            ctx.restore();
            ctx.beginPath();
            ctx.moveTo(0, rowStart + rowSize);
            ctx.lineTo(HEADER_WIDTH, rowStart + rowSize);
            ctx.stroke();
        }
    }
    drawFrozenPanesHeaders(renderingContext) {
        const { ctx, thinLineWidth } = renderingContext;
        const { x: offsetCorrectionX, y: offsetCorrectionY } = this.getters.getMainViewportCoordinates();
        const widthCorrection = this.getters.isDashboard() ? 0 : HEADER_WIDTH;
        const heightCorrection = this.getters.isDashboard() ? 0 : HEADER_HEIGHT;
        ctx.lineWidth = 6 * thinLineWidth;
        ctx.strokeStyle = FROZEN_PANE_HEADER_BORDER_COLOR;
        ctx.beginPath();
        if (offsetCorrectionX) {
            ctx.moveTo(widthCorrection + offsetCorrectionX, 0);
            ctx.lineTo(widthCorrection + offsetCorrectionX, heightCorrection);
        }
        if (offsetCorrectionY) {
            ctx.moveTo(0, heightCorrection + offsetCorrectionY);
            ctx.lineTo(widthCorrection, heightCorrection + offsetCorrectionY);
        }
        ctx.stroke();
    }
    drawFrozenPanes(renderingContext) {
        const { ctx, thinLineWidth } = renderingContext;
        const { x: offsetCorrectionX, y: offsetCorrectionY } = this.getters.getMainViewportCoordinates();
        const visibleCols = this.getters.getSheetViewVisibleCols();
        const left = visibleCols[0];
        const right = visibleCols[visibleCols.length - 1];
        const visibleRows = this.getters.getSheetViewVisibleRows();
        const top = visibleRows[0];
        const bottom = visibleRows[visibleRows.length - 1];
        const viewport = { left, right, top, bottom };
        const rect = this.getters.getVisibleRect(viewport);
        const widthCorrection = this.getters.isDashboard() ? 0 : HEADER_WIDTH;
        const heightCorrection = this.getters.isDashboard() ? 0 : HEADER_HEIGHT;
        ctx.lineWidth = 6 * thinLineWidth;
        ctx.strokeStyle = FROZEN_PANE_BORDER_COLOR;
        ctx.beginPath();
        if (offsetCorrectionX) {
            ctx.moveTo(widthCorrection + offsetCorrectionX, heightCorrection);
            ctx.lineTo(widthCorrection + offsetCorrectionX, rect.height + heightCorrection);
        }
        if (offsetCorrectionY) {
            ctx.moveTo(widthCorrection, heightCorrection + offsetCorrectionY);
            ctx.lineTo(rect.width + widthCorrection, heightCorrection + offsetCorrectionY);
        }
        ctx.stroke();
    }
    findNextEmptyCol(base, max, row) {
        const sheetId = this.getters.getActiveSheetId();
        let col = base;
        while (col < max) {
            const position = { sheetId, col: col + 1, row };
            const nextCell = this.getters.getEvaluatedCell(position);
            const nextCellBorder = this.getters.getCellComputedBorder(position);
            const doesCellHaveGridIcon = this.getters.doesCellHaveGridIcon(position);
            if (nextCell.type !== CellValueType.empty ||
                this.getters.isInMerge(position) ||
                nextCellBorder?.left ||
                doesCellHaveGridIcon) {
                return col;
            }
            col++;
        }
        return col;
    }
    findPreviousEmptyCol(base, min, row) {
        const sheetId = this.getters.getActiveSheetId();
        let col = base;
        while (col > min) {
            const position = { sheetId, col: col - 1, row };
            const previousCell = this.getters.getEvaluatedCell(position);
            const previousCellBorder = this.getters.getCellComputedBorder(position);
            const doesCellHaveGridIcon = this.getters.doesCellHaveGridIcon(position);
            if (previousCell.type !== CellValueType.empty ||
                this.getters.isInMerge(position) ||
                previousCellBorder?.right ||
                doesCellHaveGridIcon) {
                return col;
            }
            col--;
        }
        return col;
    }
    computeCellAlignment(position, isOverflowing) {
        const cell = this.getters.getCell(position);
        if (cell?.isFormula && this.getters.shouldShowFormulas()) {
            return "left";
        }
        const { align } = this.getters.getCellStyle(position);
        const evaluatedCell = this.getters.getEvaluatedCell(position);
        if (isOverflowing && evaluatedCell.type === CellValueType.number) {
            return align !== "center" ? "left" : align;
        }
        return align || evaluatedCell.defaultAlign;
    }
    createZoneBox(sheetId, zone, viewport) {
        const { left, right } = viewport;
        const col = zone.left;
        const row = zone.top;
        const position = { sheetId, col, row };
        const cell = this.getters.getEvaluatedCell(position);
        const showFormula = this.getters.shouldShowFormulas();
        const { x, y, width, height } = this.getters.getRect(zone);
        const chipStyle = this.getters.getDataValidationChipStyle(position);
        let style = this.getters.getCellComputedStyle(position);
        if (this.fingerprints.isEnabled) {
            const fingerprintColor = this.fingerprints.colors.get(position);
            style = { ...style, fillColor: fingerprintColor };
        }
        if (chipStyle?.textColor) {
            style = { ...style, textColor: chipStyle.textColor };
        }
        const dataBarFill = this.fingerprints.isEnabled
            ? undefined
            : this.getters.getConditionalDataBar(position);
        const iconsList = this.getters.getCellIcons(position);
        const cellIcons = {
            left: iconsList.find((icon) => icon?.horizontalAlign === "left"),
            right: iconsList.find((icon) => icon?.horizontalAlign === "right"),
            center: iconsList.find((icon) => icon?.horizontalAlign === "center"),
        };
        const box = {
            id: zoneToXc(zone),
            x,
            y,
            width,
            height,
            border: this.getters.getCellComputedBorder(position) || undefined,
            style,
            dataBarFill,
            overlayColor: this.hoveredTables.overlayColors.get(position),
            isError: (cell.type === CellValueType.error && !!cell.message) ||
                this.getters.isDataValidationInvalid(position),
            icons: cellIcons,
            disabledAnimation: this.zonesWithPreventedAnimationsInNextFrame.some((z) => isZoneInside(zone, z) || overlap(zone, z)),
        };
        const fontSizePX = computeTextFontSizeInPixels(box.style);
        if (cell.type === CellValueType.empty || box.icons.center) {
            return box;
        }
        /** Content */
        const wrapping = style.wrapping || "overflow";
        const wrapText = wrapping === "wrap" && !showFormula;
        // we want to give priority to the style, so if there is one we don't fill with white spaces
        const maxWidth = style.align ? 0 : width - 2 * MIN_CELL_TEXT_MARGIN;
        const multiLineText = this.getters.getCellMultiLineText(position, { maxWidth, wrapText });
        const textWidth = Math.max(...multiLineText.map((line) => this.getters.getTextWidth(line, style) + MIN_CELL_TEXT_MARGIN));
        const chipMargin = chipStyle ? DATA_VALIDATION_CHIP_MARGIN : 0;
        const leftIconWidth = box.icons.left ? box.icons.left.size + box.icons.left.margin : 0;
        const leftMargin = leftIconWidth + chipMargin;
        const rightIconWidth = box.icons.right ? box.icons.right.size + box.icons.right.margin : 0;
        const rightMargin = rightIconWidth + chipMargin;
        const contentWidth = leftMargin + textWidth + rightMargin;
        const align = this.computeCellAlignment(position, contentWidth > width);
        // compute vertical align start point parameter:
        const numberOfLines = multiLineText.length;
        const contentY = Math.round(this.getters.computeTextYCoordinate(box, fontSizePX, style.verticalAlign, numberOfLines));
        // compute horizontal align start point parameter
        let contentX = box.x;
        if (align === "left") {
            contentX += MIN_CELL_TEXT_MARGIN + leftMargin;
        }
        else if (align === "right") {
            contentX += box.width - MIN_CELL_TEXT_MARGIN - rightMargin;
        }
        else {
            contentX += box.width / 2;
        }
        contentX = Math.round(contentX);
        const textHeight = computeTextLinesHeight(fontSizePX, numberOfLines);
        box.content = {
            textLines: multiLineText,
            width: wrapping === "overflow" ? textWidth : width,
            align,
            x: contentX,
            y: contentY,
            fontSizePx: fontSizePX,
        };
        if (chipStyle?.fillColor) {
            const chipMarginLeft = leftMargin;
            const chipMarginRight = DATA_VALIDATION_CHIP_MARGIN;
            box.chip = {
                color: chipStyle.fillColor,
                width: box.width - chipMarginLeft - chipMarginRight,
                height: textHeight + 2,
                x: box.x + chipMarginLeft,
                y: contentY - 2,
            };
        }
        /** ClipRect */
        const isOverflowing = contentWidth > width || fontSizePX > height;
        if (box.icons.left || box.icons.right || box.chip) {
            box.clipRect = {
                x: box.x + leftMargin,
                y: box.y,
                width: Math.max(0, width - leftMargin - rightMargin),
                height,
            };
        }
        else if (isOverflowing && wrapping === "overflow") {
            let nextColIndex, previousColIndex;
            const isCellInMerge = this.getters.isInMerge(position);
            if (isCellInMerge) {
                // Always clip merges
                nextColIndex = this.getters.getMerge(position).right;
                previousColIndex = col;
            }
            else {
                nextColIndex = box.border?.right ? zone.right : this.findNextEmptyCol(col, right, row);
                previousColIndex = box.border?.left ? zone.left : this.findPreviousEmptyCol(col, left, row);
                box.isOverflow = true;
            }
            switch (align) {
                case "left": {
                    const emptyZoneOnTheLeft = positionToZone({ col: nextColIndex, row });
                    const { x, y, width, height } = this.getters.getVisibleRect(union(zone, emptyZoneOnTheLeft));
                    if (width < contentWidth || fontSizePX > height || multiLineText.length > 1) {
                        box.clipRect = { x, y, width, height };
                    }
                    break;
                }
                case "right": {
                    const emptyZoneOnTheRight = positionToZone({ col: previousColIndex, row });
                    const { x, y, width, height } = this.getters.getVisibleRect(union(zone, emptyZoneOnTheRight));
                    if (width < contentWidth || fontSizePX > height || multiLineText.length > 1) {
                        box.clipRect = { x, y, width, height };
                    }
                    break;
                }
                case "center": {
                    const emptyZone = {
                        ...zone,
                        left: previousColIndex,
                        right: nextColIndex,
                    };
                    const { x, y, height, width } = this.getters.getVisibleRect(emptyZone);
                    const halfContentWidth = contentWidth / 2;
                    const boxMiddle = box.x + box.width / 2;
                    if (x + width < boxMiddle + halfContentWidth ||
                        x > boxMiddle - halfContentWidth ||
                        fontSizePX > height ||
                        multiLineText.length > 1) {
                        const clipX = x > boxMiddle - halfContentWidth ? x : boxMiddle - halfContentWidth;
                        const clipWidth = x + width - clipX;
                        box.clipRect = { x: clipX, y, width: clipWidth, height };
                    }
                    break;
                }
            }
        }
        else if (wrapping === "clip" || wrapping === "wrap" || multiLineText.length > 1) {
            box.clipRect = {
                x: box.x,
                y: box.y,
                width,
                height,
            };
        }
        return box;
    }
    getGridBoxes(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 row of visibleRows) {
            for (const col of visibleCols) {
                const position = { sheetId, col, row };
                if (this.getters.isInMerge(position)) {
                    continue;
                }
                boxes.push(this.createZoneBox(sheetId, positionToZone(position), viewport));
            }
        }
        for (const merge of this.getters.getMerges(sheetId)) {
            if (this.getters.isMergeHidden(sheetId, merge)) {
                continue;
            }
            if (overlap(merge, viewport)) {
                const box = this.createZoneBox(sheetId, merge, viewport);
                const borderBottomRight = this.getters.getCellComputedBorder({
                    sheetId,
                    col: merge.right,
                    row: merge.bottom,
                });
                box.border = {
                    ...box.border,
                    bottom: borderBottomRight ? borderBottomRight.bottom : undefined,
                    right: borderBottomRight ? borderBottomRight.right : undefined,
                };
                box.isMerge = true;
                boxes.push(box);
            }
        }
        return boxes;
    }
    getBoxesWithAnimations(boxes, oldBoxes, timeStamp) {
        this.updateAnimationsProgress(timeStamp);
        this.addNewAnimations(boxes, oldBoxes, timeStamp);
        if (this.animations.size > 0) {
            this.renderer.startAnimation("grid_renderer_animation");
            return this.updateBoxesWithAnimations(boxes);
        }
        else {
            this.renderer.stopAnimation("grid_renderer_animation");
            return boxes;
        }
    }
    updateBoxesWithAnimations(boxes) {
        const boxesWithAnimations = [];
        for (const box of boxes) {
            const animation = this.animations.get(box.id);
            if (!animation) {
                boxesWithAnimations.push(box);
                continue;
            }
            const animatedBox = deepCopy(box);
            boxesWithAnimations.push(animatedBox);
            for (const animationId of animation.animationTypes) {
                const animationItem = cellAnimationRegistry.get(animationId);
                const newBoxes = animationItem.updateAnimation(animation.progress, animatedBox, animation.oldBox, box);
                if (newBoxes) {
                    boxesWithAnimations.push(...newBoxes.newBoxes);
                }
            }
        }
        return boxesWithAnimations;
    }
    updateAnimationsProgress(timeStamp) {
        if (timeStamp === undefined) {
            return;
        }
        for (const boxId of this.animations.keys()) {
            const animation = this.animations.get(boxId);
            if (animation.startTime === undefined) {
                animation.startTime = timeStamp;
                continue;
            }
            const elapsedTime = timeStamp - animation.startTime;
            const progress = Math.min(1, elapsedTime / CELL_ANIMATION_DURATION);
            if (progress >= 1) {
                this.animations.delete(boxId);
            }
            animation.progress = progress;
        }
    }
    addNewAnimations(boxes, oldBoxes, timeStamp) {
        for (const box of boxes) {
            this.lastRenderBoxes.set(box.id, box);
            const oldBox = oldBoxes.get(box.id);
            if (this.preventNewAnimationsInNextFrame || !oldBox || box.disabledAnimation) {
                continue;
            }
            const animationTypes = [];
            for (const animationItem of cellAnimationRegistry.getAll()) {
                if (animationItem.hasAnimation(oldBox, box)) {
                    animationTypes.push(animationItem.id);
                }
            }
            const animation = animationTypes.length > 0
                ? { animationTypes, oldBox, progress: 0, startTime: timeStamp }
                : undefined;
            if (animation) {
                this.animations.set(box.id, animation);
            }
        }
    }
}

function useGridDrawing(refName, model, canvasSize) {
    const canvasRef = useRef(refName);
    useEffect(drawGrid);
    const rendererStore = useStore(RendererStore);
    useStore(GridRenderer);
    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);
        rendererStore.draw(renderingContext);
    }
}

const friction = 0.95;
const verticalScrollFactor = 1;
const horizontalScrollFactor = 1;
const resetTimeoutDuration = 100;
function useTouchScroll(ref, updateScroll, canMoveUp, canMoveDown) {
    let lastX = 0;
    let lastY = 0;
    let velocityX = 0;
    let velocityY = 0;
    let isMouseDown = false;
    let lastTime = 0;
    let resetTimeout = null;
    useRefListener(ref, "touchstart", onTouchStart, { capture: false });
    useRefListener(ref, "touchmove", onTouchMove, { capture: false });
    useRefListener(ref, "touchend", onTouchEnd, { capture: false });
    function onTouchStart(event) {
        isMouseDown = true;
        ({ clientX: lastX, clientY: lastY } = event.touches[0]);
        velocityX = 0;
        velocityY = 0;
    }
    function onTouchMove(event) {
        if (!isMouseDown)
            return;
        if (resetTimeout) {
            clearTimeout(resetTimeout);
            resetTimeout = null;
        }
        const currentTime = Date.now();
        const { clientX, clientY } = event.touches[0];
        const deltaX = lastX - clientX;
        const deltaY = lastY - clientY;
        const elapsedTime = currentTime - lastTime;
        if (!elapsedTime) {
            return;
        }
        velocityX = deltaX / elapsedTime;
        velocityY = deltaY / elapsedTime;
        lastX = clientX;
        lastY = clientY;
        lastTime = currentTime;
        if ((deltaY < 0 && canMoveUp()) || (deltaY > 0 && canMoveDown())) {
            if (event.cancelable) {
                event.preventDefault();
            }
            event.stopPropagation();
        }
        resetTimeout = setTimeout(() => {
            velocityX = 0;
            velocityY = 0;
        }, resetTimeoutDuration);
        updateScroll(deltaX * horizontalScrollFactor, deltaY * verticalScrollFactor);
    }
    function onTouchEnd(ev) {
        isMouseDown = false;
        lastX = lastY = 0;
        if (resetTimeout) {
            clearTimeout(resetTimeout);
        }
        velocityX *= 1.2;
        velocityY *= 1.2;
        requestAnimationFrame(scroll);
    }
    function scroll() {
        if (Math.abs(velocityX) < 0.05) {
            velocityX = 0;
        }
        if (Math.abs(velocityY) < 0.05) {
            velocityY = 0;
        }
        if (!velocityX && !velocityY) {
            return;
        }
        const currentTime = Date.now();
        const elapsedTime = Math.abs(currentTime - lastTime);
        const deltaX = velocityX * elapsedTime;
        const deltaY = velocityY * elapsedTime;
        updateScroll(deltaX * horizontalScrollFactor, deltaY * verticalScrollFactor);
        lastTime = currentTime;
        velocityX *= friction;
        velocityY *= friction;
        requestAnimationFrame(scroll);
    }
}

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 Component {
    static template = "o-spreadsheet-Border";
    static props = {
        zone: Object,
        orientation: String,
        isMoving: Boolean,
        onMoveHighlight: Function,
    };
    get style() {
        const isTop = ["n", "w", "e"].includes(this.props.orientation);
        const isLeft = ["n", "w", "s"].includes(this.props.orientation);
        const isHorizontal = ["n", "s"].includes(this.props.orientation);
        const isVertical = ["w", "e"].includes(this.props.orientation);
        const z = this.props.zone;
        const margin = 2;
        const rect = this.env.model.getters.getVisibleRect(z);
        const left = rect.x;
        const right = rect.x + rect.width - 2 * margin;
        const top = rect.y;
        const bottom = rect.y + rect.height - 2 * margin;
        const lineWidth = 4;
        const leftValue = isLeft ? left : right;
        const topValue = isTop ? top : bottom;
        const widthValue = isHorizontal ? right - left : lineWidth;
        const heightValue = isVertical ? bottom - top : lineWidth;
        return cssPropertiesToCss({
            left: `${leftValue}px`,
            top: `${topValue}px`,
            width: `${widthValue}px`,
            height: `${heightValue}px`,
        });
    }
    onMouseDown(ev) {
        this.props.onMoveHighlight(ev);
    }
}

const MOBILE_HANDLER_WIDTH = 40;
css /* scss */ `
  .o-corner {
    position: absolute;
  }

  .o-corner-button {
    border: 1px solid white;
    height: ${AUTOFILL_EDGE_LENGTH}px;
    width: ${AUTOFILL_EDGE_LENGTH}px;
  }
  .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 Component {
    static template = "o-spreadsheet-Corner";
    static props = {
        zone: Object,
        color: String,
        orientation: String,
        isResizing: Boolean,
        onResizeHighlight: Function,
    };
    dirX;
    dirY;
    setup() {
        const { dirX, dirY } = orientationToDir(this.props.orientation);
        this.dirX = dirX;
        this.dirY = dirY;
    }
    get handlerStyle() {
        const z = this.props.zone;
        const rect = this.env.model.getters.getVisibleRect({
            left: this.dirX === 1 ? z.right : z.left,
            right: this.dirX === -1 ? z.left : z.right,
            top: this.dirY === 1 ? z.bottom : z.top,
            bottom: this.dirY === -1 ? z.top : z.bottom,
        });
        // Don't show if not visible in the viewport
        if (rect.width * rect.height === 0) {
            return `display: none !important;`;
        }
        const leftValue = rect.x + rect.width / 2 + (this.dirX * rect.width) / 2;
        const topValue = rect.y + rect.height / 2 + (this.dirY * rect.height) / 2;
        const edgeLength = this.getHandlerEdgeLength();
        const css = {
            left: `${leftValue - edgeLength / 2}px`,
            top: `${topValue - edgeLength / 2}px`,
            height: `${edgeLength}px`,
            width: `${edgeLength}px`,
        };
        if (this.env.isMobile()) {
            css["border-radius"] = `${edgeLength / 2}px`;
        }
        return cssPropertiesToCss(css);
    }
    getHandlerEdgeLength() {
        return this.env.isMobile() ? MOBILE_HANDLER_WIDTH : AUTOFILL_EDGE_LENGTH;
    }
    get buttonLook() {
        const css = {
            "background-color": this.props.color,
            cursor: `${this.props.orientation}-resize`,
        };
        if (this.env.isMobile()) {
            css["border-radius"] = `${AUTOFILL_EDGE_LENGTH / 2}px`;
        }
        return cssPropertiesToCss(css);
    }
    onMouseDown(ev) {
        this.props.onResizeHighlight(ev, this.dirX, this.dirY);
    }
}
function orientationToDir(or) {
    const dirX = or.includes("w") ? -1 : or.includes("e") ? 1 : 0;
    const dirY = or.includes("n") ? -1 : or.includes("s") ? 1 : 0;
    return { dirX, dirY };
}

css /*SCSS*/ `
  .o-highlight {
    z-index: ${ComponentsImportance.Highlight};
  }
`;
class Highlight extends Component {
    static template = "o-spreadsheet-Highlight";
    static props = {
        range: Object,
        color: String,
    };
    static components = {
        Corner,
        Border,
    };
    highlightState = useState({
        shiftingMode: "none",
    });
    dragNDropGrid = useDragAndDropBeyondTheViewport(this.env);
    get cornerOrientations() {
        if (!this.env.isMobile()) {
            return ["nw", "ne", "sw", "se"];
        }
        const z = this.props.range.unboundedZone;
        if (z.bottom === undefined) {
            return ["w", "e"];
        }
        else if (z.right === undefined) {
            return ["n", "s"];
        }
        else {
            return ["nw", "se"];
        }
    }
    onResizeHighlight(ev, dirX, dirY) {
        const activeSheetId = this.env.model.getters.getActiveSheetId();
        this.highlightState.shiftingMode = "isResizing";
        const z = this.props.range.zone;
        const pivotCol = dirX === 1 ? z.left : z.right;
        const pivotRow = dirY === 1 ? z.top : z.bottom;
        let lastCol = dirX === 1 ? z.right : z.left;
        let lastRow = dirY === 1 ? z.bottom : z.top;
        let currentZone = z;
        let scrollDirection = "all";
        if (this.env.isMobile()) {
            scrollDirection = dirX === 0 ? "vertical" : dirY === 0 ? "horizontal" : "all";
        }
        this.env.model.dispatch("START_CHANGE_HIGHLIGHT", { zone: currentZone });
        const mouseMove = (col, row) => {
            if (lastCol !== col || lastRow !== row) {
                let { left, right, top, bottom } = currentZone;
                if (scrollDirection !== "horizontal") {
                    lastRow = lastRow = clip(row === -1 ? lastRow : row, 0, this.env.model.getters.getNumberRows(activeSheetId) - 1);
                    top = Math.min(pivotRow, lastRow);
                    bottom = Math.max(pivotRow, lastRow);
                }
                if (scrollDirection !== "vertical") {
                    lastCol = clip(col === -1 ? lastCol : col, 0, this.env.model.getters.getNumberCols(activeSheetId) - 1);
                    left = Math.min(pivotCol, lastCol);
                    right = Math.max(pivotCol, lastCol);
                }
                const newZone = { left, right, top, bottom };
                if (!isEqual(newZone, currentZone)) {
                    this.env.model.selection.selectZone({
                        cell: { col: newZone.left, row: newZone.top },
                        zone: newZone,
                    }, { unbounded: true });
                    currentZone = newZone;
                }
            }
        };
        const mouseUp = () => {
            this.highlightState.shiftingMode = "none";
        };
        this.dragNDropGrid.start(ev, mouseMove, mouseUp, scrollDirection);
    }
    onMoveHighlight(ev) {
        this.highlightState.shiftingMode = "isMoving";
        const z = this.props.range.zone;
        const position = gridOverlayPosition();
        const activeSheetId = this.env.model.getters.getActiveSheetId();
        const initCol = this.env.model.getters.getColIndex(ev.clientX - position.left);
        const initRow = this.env.model.getters.getRowIndex(ev.clientY - position.top);
        const deltaColMin = -z.left;
        const deltaColMax = this.env.model.getters.getNumberCols(activeSheetId) - z.right - 1;
        const deltaRowMin = -z.top;
        const deltaRowMax = this.env.model.getters.getNumberRows(activeSheetId) - z.bottom - 1;
        let currentZone = z;
        this.env.model.dispatch("START_CHANGE_HIGHLIGHT", { zone: currentZone });
        let lastCol = initCol;
        let lastRow = initRow;
        const mouseMove = (col, row) => {
            if (lastCol !== col || lastRow !== row) {
                lastCol = col === -1 ? lastCol : col;
                lastRow = row === -1 ? lastRow : row;
                const deltaCol = clip(lastCol - initCol, deltaColMin, deltaColMax);
                const deltaRow = clip(lastRow - initRow, deltaRowMin, deltaRowMax);
                const newZone = {
                    left: z.left + deltaCol,
                    top: z.top + deltaRow,
                    right: z.right + deltaCol,
                    bottom: z.bottom + deltaRow,
                };
                if (!isEqual(newZone, currentZone)) {
                    this.env.model.selection.selectZone({
                        cell: { col: newZone.left, row: newZone.top },
                        zone: newZone,
                    }, { unbounded: true });
                    currentZone = newZone;
                }
            }
        };
        const mouseUp = () => {
            this.highlightState.shiftingMode = "none";
        };
        this.dragNDropGrid.start(ev, mouseMove, mouseUp);
    }
}

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

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

    &.corner {
      box-sizing: content-box;
      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 Component {
    static props = {
        width: { type: Number, optional: true },
        height: { type: Number, optional: true },
        direction: String,
        position: Object,
        offset: Number,
        onScroll: Function,
    };
    static template = xml /*xml*/ `
    <div
        t-attf-class="o-scrollbar {{props.direction}}"
        t-on-scroll="onScroll"
        t-ref="scrollbar"
        t-att-style="positionCss">
      <div t-att-style="sizeCss"/>
    </div>
  `;
    static defaultProps = {
        width: 1,
        height: 1,
    };
    scrollbarRef;
    scrollbar;
    setup() {
        this.scrollbarRef = useRef("scrollbar");
        this.scrollbar = new ScrollBar$1(this.scrollbarRef.el, this.props.direction);
        onMounted(() => {
            this.scrollbar.el = this.scrollbarRef.el;
        });
        // TODO improve useEffect dependencies typing in 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);
        }
    }
}

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

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

class Selection extends Component {
    static template = "o-spreadsheet-Selection";
    static props = {};
    static components = { Highlight };
    get highlightProps() {
        const sheetId = this.env.model.getters.getActiveSheetId();
        const zone = this.env.model.getters.getUnboundedZone(sheetId, this.env.model.getters.getSelectedZone());
        const range = this.env.model.getters.getRangeFromZone(sheetId, zone);
        return { range, color: SELECTION_BORDER_COLOR };
    }
}

function useAutofocus({ refName }) {
    const ref = useRef(refName);
    useEffect((el) => {
        el?.focus();
    }, () => [ref.el]);
}

css /* scss */ `
  .o-spreadsheet {
    .os-input {
      border-width: 0 0 1px 0;
      border-color: transparent;
      outline: none;
      text-overflow: ellipsis;
      color: ${TEXT_BODY};
    }
    .os-input:hover,
    .os-input.o-input-border {
      border-color: ${GRAY_300};
    }
    .os-input:focus {
      border-color: ${ACTION_COLOR};
    }
  }
`;
class TextInput extends Component {
    static template = "o-spreadsheet-TextInput";
    static props = {
        value: String,
        onChange: Function,
        class: {
            type: String,
            optional: true,
        },
        id: {
            type: String,
            optional: true,
        },
        placeholder: {
            type: String,
            optional: true,
        },
        autofocus: {
            type: Boolean,
            optional: true,
        },
        alwaysShowBorder: { type: Boolean, optional: true },
    };
    inputRef = useRef("input");
    setup() {
        useExternalListener(window, "click", (ev) => {
            if (ev.target !== this.inputRef.el && this.inputRef.el?.value !== this.props.value) {
                this.save();
            }
        }, { capture: true });
        if (this.props.autofocus) {
            useAutofocus({ refName: "input" });
        }
    }
    onKeyDown(ev) {
        switch (ev.key) {
            case "Enter":
                this.save();
                ev.preventDefault();
                ev.stopPropagation();
                break;
            case "Escape":
                if (this.inputRef.el) {
                    this.inputRef.el.value = this.props.value;
                    this.inputRef.el.blur();
                }
                ev.preventDefault();
                ev.stopPropagation();
                break;
        }
    }
    save() {
        const currentValue = (this.inputRef.el?.value || "").trim();
        if (currentValue !== this.props.value) {
            this.props.onChange(currentValue);
        }
        this.inputRef.el?.blur();
    }
    onMouseDown(ev) {
        // Stop the event if the input is not focused, we handle everything in onMouseUp
        if (ev.target !== document.activeElement) {
            ev.preventDefault();
            ev.stopPropagation();
        }
    }
    onMouseUp(ev) {
        const target = ev.target;
        if (target !== document.activeElement) {
            target.focus();
            target.select();
            ev.preventDefault();
            ev.stopPropagation();
        }
    }
    get inputClass() {
        return [this.props.class, this.props.alwaysShowBorder ? "o-input-border" : undefined]
            .filter(isDefined)
            .join(" ");
    }
}

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

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

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

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

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

css /* scss */ `
  .o-font-size-editor {
    width: max-content !important;
    height: calc(100% - 4px);
    input.o-font-size {
      outline: none;
      height: 20px;
      width: 31px;
      text-align: center;
    }
  }
  .o-text-options > div {
    cursor: pointer;
    line-height: 26px;
    padding: 3px 12px;
    &:hover {
      background-color: rgba(0, 0, 0, 0.08);
    }
  }
`;
class FontSizeEditor extends Component {
    static template = "o-spreadsheet-FontSizeEditor";
    static props = {
        currentFontSize: Number,
        onFontSizeChanged: Function,
        onToggle: { type: Function, optional: true },
        onFocusInput: { type: Function, optional: true },
        class: String,
    };
    static defaultProps = {
        onFocusInput: () => { },
    };
    static components = { Popover };
    fontSizes = FONT_SIZES;
    dropdown = useState({ isOpen: false });
    inputRef = useRef("inputFontSize");
    rootEditorRef = useRef("FontSizeEditor");
    fontSizeListRef = useRef("fontSizeList");
    DOMFocusableElementStore;
    setup() {
        this.DOMFocusableElementStore = useStore(DOMFocusableElementStore);
        useExternalListener(window, "click", this.onExternalClick, { capture: true });
    }
    get popoverProps() {
        const { x, y, width, height } = this.rootEditorRef.el.getBoundingClientRect();
        return {
            anchorRect: { x, y, width, height },
            positioning: "bottom-left",
            verticalOffset: 0,
        };
    }
    onExternalClick(ev) {
        if (!isChildEvent(this.fontSizeListRef.el, ev) && !isChildEvent(this.rootEditorRef.el, ev)) {
            this.closeFontList();
        }
    }
    toggleFontList() {
        const isOpen = this.dropdown.isOpen;
        if (!isOpen) {
            this.props.onToggle?.();
            this.inputRef.el.focus();
        }
        else {
            this.closeFontList();
        }
    }
    closeFontList() {
        this.dropdown.isOpen = false;
    }
    setSize(fontSizeStr) {
        const fontSize = clip(Math.floor(parseFloat(fontSizeStr)), 1, 400);
        this.props.onFontSizeChanged(fontSize);
        this.closeFontList();
    }
    setSizeFromInput(ev) {
        this.setSize(ev.target.value);
    }
    setSizeFromList(fontSizeStr) {
        this.setSize(fontSizeStr);
    }
    onInputFocused(ev) {
        this.dropdown.isOpen = true;
        ev.target.select();
    }
    onInputKeydown(ev) {
        if (ev.key === "Enter" || ev.key === "Escape") {
            this.closeFontList();
            const target = ev.target;
            // In the case of a ESCAPE key, we get the previous font size back
            if (ev.key === "Escape") {
                target.value = `${this.props.currentFontSize}`;
            }
            this.props.onToggle?.();
        }
        if (ev.key === "Tab") {
            ev.preventDefault();
            ev.stopPropagation();
            this.closeFontList();
            this.DOMFocusableElementStore.focus();
            return;
        }
    }
}

css /* scss */ `
  .o-chart-title-designer {
    .o-hoverable-button {
      height: 30px;
    }
    .o-dropdown-content .o-hoverable-button {
      height: fit-content;
    }

    .o-divider {
      border-right: 1px solid ${GRAY_300};
      margin: 0px 4px;
      height: 14px;
    }

    .o-menu-item-button.active {
      background-color: #e6f4ea;
      color: #188038;
    }

    .o-dropdown-content {
      overflow-y: auto;
      overflow-x: hidden;
      padding: 2px;
      z-index: 100;
      box-shadow: 1px 2px 5px 2px rgba(51, 51, 51, 0.15);

      .o-dropdown-line {
        > span {
          padding: 4px;
        }
      }
    }
  }
`;
class TextStyler extends Component {
    static template = "o-spreadsheet.TextStyler";
    static components = { ColorPickerWidget, ActionButton, FontSizeEditor };
    static props = {
        style: Object,
        updateStyle: { type: Function, optional: true },
        defaultStyle: { type: Object, optional: true },
        hasVerticalAlign: { type: Boolean, optional: true },
        hasHorizontalAlign: { type: Boolean, optional: true },
        hasBackgroundColor: { type: Boolean, optional: true },
        class: { type: String, optional: true },
    };
    openedEl = null;
    setup() {
        useExternalListener(window, "click", this.onExternalClick);
    }
    state = useState({
        activeTool: "",
    });
    updateFontSize(fontSize) {
        this.props.updateStyle?.({ ...this.props.style, fontSize });
    }
    toggleDropdownTool(tool, ev) {
        const isOpen = this.state.activeTool === tool;
        this.closeMenus();
        this.state.activeTool = isOpen ? "" : tool;
        this.openedEl = isOpen ? null : ev.target;
    }
    /**
     * TODO: This is clearly not a goot way to handle external click, but
     * we currently have no other way to do it ... Should be done in
     * another task to handle the fact we want only one menu opened at a
     * time with something like a menuStore ?
     */
    onExternalClick(ev) {
        if (this.openedEl === ev.target) {
            return;
        }
        this.closeMenus();
    }
    onTextColorChange(color) {
        this.props.updateStyle?.({ ...this.props.style, color });
        this.closeMenus();
    }
    onFillColorChange(color) {
        this.props.updateStyle?.({ ...this.props.style, fillColor: color });
        this.closeMenus();
    }
    updateAlignment(align) {
        this.props.updateStyle?.({ ...this.props.style, align });
        this.closeMenus();
    }
    updateVerticalAlignment(verticalAlign) {
        this.props.updateStyle?.({ ...this.props.style, verticalAlign });
        this.closeMenus();
    }
    toggleBold() {
        this.props.updateStyle?.({ ...this.props.style, bold: !this.bold });
    }
    toggleItalic() {
        this.props.updateStyle?.({ ...this.props.style, italic: !this.italic });
    }
    closeMenus() {
        this.state.activeTool = "";
        this.openedEl = null;
    }
    get align() {
        return this.props.style.align ?? this.props.defaultStyle?.align;
    }
    get verticalAlign() {
        return this.props.style.verticalAlign || this.props.defaultStyle?.verticalAlign;
    }
    get bold() {
        return this.props.style.bold ?? this.props.defaultStyle?.bold;
    }
    get italic() {
        return this.props.style.italic ?? this.props.defaultStyle?.italic;
    }
    get currentFontSize() {
        return this.props.style.fontSize ?? this.props.defaultStyle?.fontSize ?? DEFAULT_STYLE.fontSize;
    }
    get boldButtonAction() {
        return {
            name: _t("Bold"),
            execute: () => this.toggleBold(),
            isActive: () => this.bold || false,
            icon: "o-spreadsheet-Icon.BOLD",
        };
    }
    get italicButtonAction() {
        return {
            name: _t("Italic"),
            execute: () => this.toggleItalic(),
            isActive: () => this.italic || false,
            icon: "o-spreadsheet-Icon.ITALIC",
        };
    }
    get horizontalAlignButtonAction() {
        let icon = "o-spreadsheet-Icon.ALIGN_LEFT";
        if (this.align === "center") {
            icon = "o-spreadsheet-Icon.ALIGN_CENTER";
        }
        else if (this.align === "right") {
            icon = "o-spreadsheet-Icon.ALIGN_RIGHT";
        }
        return { name: _t("Horizontal alignment"), icon };
    }
    get horizontalAlignActions() {
        return [
            {
                name: _t("Left"),
                execute: () => this.updateAlignment("left"),
                isActive: () => this.align === "left",
                icon: "o-spreadsheet-Icon.ALIGN_LEFT",
            },
            {
                name: _t("Center"),
                execute: () => this.updateAlignment("center"),
                isActive: () => this.align === "center",
                icon: "o-spreadsheet-Icon.ALIGN_CENTER",
            },
            {
                name: _t("Right"),
                execute: () => this.updateAlignment("right"),
                isActive: () => this.align === "right",
                icon: "o-spreadsheet-Icon.ALIGN_RIGHT",
            },
        ];
    }
    get verticalAlignButtonAction() {
        let icon = "o-spreadsheet-Icon.ALIGN_MIDDLE";
        if (this.verticalAlign === "top") {
            icon = "o-spreadsheet-Icon.ALIGN_TOP";
        }
        else if (this.verticalAlign === "bottom") {
            icon = "o-spreadsheet-Icon.ALIGN_BOTTOM";
        }
        return { name: _t("Vertical alignment"), icon };
    }
    get verticalAlignActions() {
        return [
            {
                name: _t("Top"),
                execute: () => this.updateVerticalAlignment("top"),
                isActive: () => this.verticalAlign === "top",
                icon: "o-spreadsheet-Icon.ALIGN_TOP",
            },
            {
                name: _t("Middle"),
                execute: () => this.updateVerticalAlignment("middle"),
                isActive: () => this.verticalAlign === "middle",
                icon: "o-spreadsheet-Icon.ALIGN_MIDDLE",
            },
            {
                name: _t("Bottom"),
                execute: () => this.updateVerticalAlignment("bottom"),
                isActive: () => this.verticalAlign === "bottom",
                icon: "o-spreadsheet-Icon.ALIGN_BOTTOM",
            },
        ];
    }
}

class CogWheelMenu extends Component {
    static template = "o-spreadsheet-CogWheelMenu";
    static components = { MenuPopover };
    static props = {
        items: Array,
    };
    buttonRef = useRef("button");
    menuState = useState({ isOpen: false, anchorRect: null, menuItems: [] });
    menuId = this.env.model.uuidGenerator.uuidv4();
    toggleMenu(ev) {
        if (ev.closedMenuId === this.menuId) {
            return;
        }
        this.menuState.isOpen = !this.menuState.isOpen;
        this.menuState.anchorRect = getBoundingRectAsPOJO(this.buttonRef.el);
        this.menuState.menuItems = createActions(this.props.items);
    }
}

class CarouselPanel extends Component {
    static template = "o-spreadsheet-CarouselPanel";
    static props = { onCloseSidePanel: Function, figureId: String };
    static components = { Section, TextInput, TextStyler, CogWheelMenu };
    DEFAULT_CAROUSEL_TITLE_STYLE = DEFAULT_CAROUSEL_TITLE_STYLE;
    dragAndDrop = useDragAndDropListItems();
    previewListRef = useRef("previewList");
    setup() {
        let lastCarouselItems = [...this.carouselItems];
        onWillUpdateProps(() => {
            if (!deepEquals(this.carouselItems, lastCarouselItems)) {
                this.dragAndDrop.cancel();
            }
            lastCarouselItems = [...this.carouselItems];
        });
    }
    get carouselItems() {
        return this.env.model.getters.getCarousel(this.props.figureId).items;
    }
    get title() {
        return this.env.model.getters.getCarousel(this.props.figureId).title;
    }
    get carousel() {
        return this.env.model.getters.getCarousel(this.props.figureId);
    }
    getPreviewDivStyle(item) {
        return this.dragAndDrop.itemsStyle[this.getItemId(item)] || "";
    }
    getItemId(item) {
        return item.type === "chart" ? item.chartId : "transparent-carousel";
    }
    addNewChartToCarousel() {
        this.env.model.dispatch("ADD_NEW_CHART_TO_CAROUSEL", {
            figureId: this.props.figureId,
            sheetId: this.carouselSheetId,
        });
    }
    get hasDataView() {
        return this.carouselItems.some((item) => item.type === "carouselDataView");
    }
    isCarouselItemActive(item) {
        const activeItem = this.env.model.getters.getSelectedCarouselItem(this.props.figureId);
        return deepEquals(activeItem, item);
    }
    addDataViewToCarousel() {
        const carousel = this.env.model.getters.getCarousel(this.props.figureId);
        this.updateItems([...carousel.items, { type: "carouselDataView" }]);
    }
    activateCarouselItem(item) {
        this.env.model.dispatch("UPDATE_CAROUSEL_ACTIVE_ITEM", {
            figureId: this.props.figureId,
            sheetId: this.carouselSheetId,
            item,
        });
    }
    editCarouselItem(item) {
        if (item.type === "chart") {
            this.activateCarouselItem(item);
            this.env.model.dispatch("SELECT_FIGURE", { figureId: this.props.figureId });
            this.env.openSidePanel("ChartPanel", { chartId: item.chartId });
        }
    }
    renameCarouselItem(item, newName) {
        const trimmedName = newName.trim();
        if (!trimmedName || trimmedName === this.getItemTitle(item).toString()) {
            return;
        }
        const items = [...this.carouselItems];
        const itemIndex = this.carouselItems.findIndex((itm) => deepEquals(itm, item));
        if (itemIndex !== -1) {
            items[itemIndex] = { ...item, title: trimmedName };
            this.updateItems(items);
        }
    }
    deleteCarouselItem(item) {
        const carousel = this.env.model.getters.getCarousel(this.props.figureId);
        const items = carousel.items.filter((itm) => !deepEquals(itm, item));
        this.updateItems(items);
    }
    popOutCarouselItem(item) {
        if (item.type !== "chart")
            return;
        this.env.model.dispatch("POPOUT_CHART_FROM_CAROUSEL", {
            sheetId: this.carouselSheetId,
            carouselId: this.props.figureId,
            chartId: item.chartId,
        });
    }
    onDragHandleMouseDown(item, event) {
        if (event.button !== 0)
            return;
        const previewRects = Array.from(this.previewListRef.el.children).map((previewEl) => getBoundingRectAsPOJO(previewEl));
        const items = this.carouselItems.map((item, index) => ({
            id: this.getItemId(item),
            size: previewRects[index].height,
            position: previewRects[index].y,
        }));
        this.dragAndDrop.start("vertical", {
            draggedItemId: this.getItemId(item),
            initialMousePosition: event.clientY,
            items: items,
            scrollableContainerEl: this.previewListRef.el,
            onDragEnd: (itemId, finalIndex) => this.onDragEnd(item, finalIndex),
        });
    }
    onDragEnd(item, finalIndex) {
        const originalIndex = this.carouselItems.findIndex((itm) => deepEquals(itm, item));
        if (originalIndex === -1 || originalIndex === finalIndex) {
            return;
        }
        const carousel = this.env.model.getters.getCarousel(this.props.figureId);
        const items = [...carousel.items];
        items.splice(originalIndex, 1);
        items.splice(finalIndex, 0, item);
        this.updateItems(items);
    }
    getItemTitle(item) {
        return getCarouselItemTitle(this.env.model.getters, item);
    }
    getItemPreview(item) {
        return getCarouselItemPreview(this.env.model.getters, item);
    }
    updateItems(items) {
        this.env.model.dispatch("UPDATE_CAROUSEL", {
            figureId: this.props.figureId,
            sheetId: this.carouselSheetId,
            definition: { ...this.carousel, items },
        });
    }
    updateTitleText(title) {
        const carousel = this.env.model.getters.getCarousel(this.props.figureId);
        this.env.model.dispatch("UPDATE_CAROUSEL", {
            figureId: this.props.figureId,
            sheetId: this.carouselSheetId,
            definition: {
                ...carousel,
                title: {
                    ...carousel.title,
                    text: title,
                },
            },
        });
    }
    updateTitleStyle(style) {
        const carousel = this.env.model.getters.getCarousel(this.props.figureId);
        this.env.model.dispatch("UPDATE_CAROUSEL", {
            figureId: this.props.figureId,
            sheetId: this.carouselSheetId,
            definition: {
                ...carousel,
                title: {
                    ...carousel.title,
                    ...style,
                },
            },
        });
    }
    get carouselAddChartInfoMessage() {
        return _t("Add a chart to the carousel. You can also add a chart by dragging and dropping it over the carousel figure.");
    }
    getCogWheelMenuItems(item) {
        const actions = [];
        if (item.type === "chart") {
            actions.push({
                name: _t("Edit chart"),
                execute: () => this.editCarouselItem(item),
                icon: "o-spreadsheet-Icon.EDIT",
            });
            actions.push({
                name: _t("Pop out chart"),
                execute: () => this.popOutCarouselItem(item),
                icon: "o-spreadsheet-Icon.EXTERNAL",
            });
        }
        actions.push({
            name: _t("Delete item"),
            execute: () => this.deleteCarouselItem(item),
            icon: "o-spreadsheet-Icon.TRASH",
        });
        return actions;
    }
    get carouselSheetId() {
        const sheetId = this.env.model.getters.getFigureSheetId(this.props.figureId);
        if (!sheetId) {
            throw new Error("Could not find the sheetId of the carousel figure");
        }
        return sheetId;
    }
    get carouselDataViewMessage() {
        return _t("The data view makes the carousel transparent, revealing the data underneath.");
    }
}

class ChartDataSeries extends Component {
    static template = "o-spreadsheet.ChartDataSeries";
    static components = { SelectionInput, Section };
    static props = {
        ranges: Array,
        hasSingleRange: { type: Boolean, optional: true },
        onSelectionChanged: Function,
        onSelectionReordered: { type: Function, optional: true },
        onSelectionRemoved: { type: Function, optional: true },
        onSelectionConfirmed: Function,
        title: { type: String, optional: true },
        maxNumberOfUsedRanges: { type: Number, optional: true },
        datasetOrientation: { type: String, optional: true },
        canChangeDatasetOrientation: { type: Boolean, optional: true },
        onFlipAxis: { type: Function, optional: true },
    };
    get ranges() {
        return this.props.ranges.map((r) => r.dataRange);
    }
    get disabledRanges() {
        return this.props.ranges.map((r, i) => this.props.maxNumberOfUsedRanges ? i >= this.props.maxNumberOfUsedRanges : false);
    }
    get colors() {
        return this.props.ranges.map((r) => r.backgroundColor);
    }
    get title() {
        if (this.props.title) {
            return this.props.title;
        }
        return this.props.hasSingleRange ? _t("Data range") : _t("Data series");
    }
}

class ChartErrorSection extends Component {
    static template = "o-spreadsheet.ChartErrorSection";
    static components = { Section, ValidationMessages };
    static props = { messages: { type: Array, element: String } };
}

class ChartLabelRange extends Component {
    static template = "o-spreadsheet.ChartLabelRange";
    static components = { SelectionInput, Checkbox, Section };
    static props = {
        title: { type: String, optional: true },
        range: String,
        isInvalid: Boolean,
        onSelectionChanged: Function,
        onSelectionConfirmed: Function,
        options: { type: Array, optional: true },
    };
    static defaultProps = {
        title: _t("Categories / Labels"),
        options: [],
    };
}

class GenericChartConfigPanel extends Component {
    static template = "o-spreadsheet-GenericChartConfigPanel";
    static components = {
        ChartDataSeries,
        ChartLabelRange,
        Section,
        Checkbox,
        ChartErrorSection,
    };
    static props = {
        chartId: String,
        definition: Object,
        updateChart: Function,
        canUpdateChart: Function,
    };
    state = useState({
        datasetDispatchResult: undefined,
        labelsDispatchResult: undefined,
    });
    dataSets = [];
    labelRange;
    datasetOrientation = undefined;
    chartTerms = ChartTerms;
    setup() {
        this.dataSets = this.props.definition.dataSets;
        this.labelRange = this.props.definition.labelRange;
        this.datasetOrientation = this.computeDatasetOrientation();
    }
    get errorMessages() {
        const cancelledReasons = [
            ...(this.state.datasetDispatchResult?.reasons || []),
            ...(this.state.labelsDispatchResult?.reasons || []),
        ].filter((reason) => reason !== "NoChanges" /* CommandResult.NoChanges */);
        return cancelledReasons.map((error) => ChartTerms.Errors[error] || ChartTerms.Errors.Unexpected);
    }
    get isDatasetInvalid() {
        return !!this.state.datasetDispatchResult?.isCancelledBecause("InvalidDataSet" /* CommandResult.InvalidDataSet */);
    }
    get isLabelInvalid() {
        return !!this.state.labelsDispatchResult?.isCancelledBecause("InvalidLabelRange" /* CommandResult.InvalidLabelRange */);
    }
    get dataSetsHaveTitleLabel() {
        return this.datasetOrientation === "rows"
            ? _t("Use col %(column_name)s as headers", {
                column_name: numberToLetters(this.calculateHeaderPosition() || 0),
            })
            : _t("Use row %(row_position)s as headers", {
                row_position: this.calculateHeaderPosition() || "",
            });
    }
    getLabelRangeOptions() {
        const definition = this.props.definition;
        return [
            {
                name: "aggregated",
                label: this.chartTerms.AggregatedChart,
                value: ("aggregated" in definition ? definition.aggregated : false) ?? false,
                onChange: this.onUpdateAggregated.bind(this),
            },
            {
                name: "dataSetsHaveTitle",
                label: this.dataSetsHaveTitleLabel,
                value: definition.dataSetsHaveTitle,
                onChange: this.onUpdateDataSetsHaveTitle.bind(this),
            },
        ];
    }
    onUpdateDataSetsHaveTitle(dataSetsHaveTitle) {
        this.props.updateChart(this.props.chartId, {
            dataSetsHaveTitle,
        });
    }
    get canChangeDatasetOrientation() {
        const sheetNames = new Set();
        const datasetZones = [];
        const currentSheetName = this.env.model.getters.getActiveSheetName();
        const ranges = this.dataSets.map((ds) => ds.dataRange);
        if (this.labelRange) {
            ranges.push(this.labelRange);
        }
        for (const range of ranges) {
            if (!isXcRepresentation(range)) {
                return false;
            }
            const reference = splitReference(range);
            const zone = toUnboundedZone(reference.xc);
            if (zone.bottom === undefined || zone.right === undefined) {
                return false;
            }
            datasetZones.push(zone);
            sheetNames.add(reference.sheetName || currentSheetName);
            if (sheetNames.size > 1) {
                return false;
            }
        }
        const mergedZones = mergeContiguousZones(datasetZones);
        if (mergedZones.length !== 1) {
            return false;
        }
        const { left, right, top, bottom } = mergedZones[0];
        if (datasetZones.some((zone) => (zone.top !== top || zone.bottom !== bottom) &&
            (zone.left !== left || zone.right !== right))) {
            return false;
        }
        return true;
    }
    computeDatasetOrientation() {
        let anyRow = false;
        let anyColumn = false;
        for (const dataSet of this.dataSets) {
            if (!isXcRepresentation(dataSet.dataRange)) {
                return undefined;
            }
            const zone = toUnboundedZone(dataSet.dataRange);
            if (zone.bottom === undefined || zone.right === undefined) {
                return undefined;
            }
            if (zone.top === zone.bottom) {
                anyRow = true;
            }
            if (zone.left === zone.right) {
                anyColumn = true;
            }
        }
        if (anyRow && !anyColumn) {
            return "rows";
        }
        else if (!anyRow && anyColumn) {
            return "columns";
        }
        return undefined;
    }
    setDatasetOrientation(datasetOrientation) {
        const oldDataSets = this.props.definition.dataSets;
        const dataRanges = oldDataSets.map((d) => d.dataRange);
        const dataSets = this.transposeDataSet([this.props.definition.labelRange, ...dataRanges], datasetOrientation);
        if (dataSets.length === 0) {
            return;
        }
        const labelRange = dataSets.length > 1 ? dataSets.shift().dataRange : "";
        this.props.updateChart(this.props.chartId, {
            labelRange,
            dataSets,
        });
        this.dataSets = dataSets;
        this.labelRange = labelRange;
        this.datasetOrientation = datasetOrientation;
    }
    /**
     * Change the local dataSeriesRanges. The model should be updated when the
     * button "confirm" is clicked
     */
    onDataSeriesRangesChanged(ranges) {
        this.dataSets = ranges.map((dataRange, i) => ({
            ...this.dataSets?.[i],
            dataRange,
        }));
        this.state.datasetDispatchResult = this.props.canUpdateChart(this.props.chartId, {
            dataSets: this.dataSets,
        });
    }
    onDataSeriesReordered(indexes) {
        const colorGenerator = getChartColorsGenerator({ dataSets: this.dataSets }, this.dataSets.length);
        this.datasetOrientation = undefined;
        const colors = this.dataSets.map((ds) => colorGenerator.next());
        this.dataSets = indexes.map((i) => ({
            backgroundColor: colors[i],
            ...this.dataSets[i],
        }));
        this.state.datasetDispatchResult = this.props.updateChart(this.props.chartId, {
            dataSets: this.dataSets,
        });
    }
    onDataSeriesRemoved(index) {
        const colorGenerator = getChartColorsGenerator({ dataSets: this.dataSets }, this.dataSets.length);
        const colors = this.dataSets.map((ds) => colorGenerator.next());
        this.dataSets = this.dataSets
            .map((ds, i) => ({
            backgroundColor: colors[i],
            ...ds,
        }))
            .filter((_, i) => i !== index);
        this.state.datasetDispatchResult = this.props.updateChart(this.props.chartId, {
            dataSets: this.dataSets,
        });
    }
    onDataSeriesConfirmed() {
        this.dataSets = this.splitRanges;
        this.datasetOrientation = this.computeDatasetOrientation();
        this.state.datasetDispatchResult = this.props.updateChart(this.props.chartId, {
            dataSets: this.dataSets,
        });
        if (this.state.datasetDispatchResult.isSuccessful) {
            this.dataSets = this.env.model.getters.getChartDefinition(this.props.chartId).dataSets;
        }
    }
    get splitRanges() {
        const postProcessedRanges = [];
        for (const dataSet of this.dataSets) {
            const range = dataSet.dataRange;
            if (!this.env.model.getters.isRangeValid(range)) {
                postProcessedRanges.push(dataSet); // ignore invalid range
                continue;
            }
            const { sheetName } = splitReference(range);
            const sheetPrefix = sheetName ? `${sheetName}!` : "";
            const zone = toUnboundedZone(range);
            if (zone.bottom !== zone.top && zone.left !== zone.right) {
                if (this.datasetOrientation !== "rows") {
                    if (zone.right !== undefined) {
                        for (let j = zone.left; j <= zone.right; ++j) {
                            const datasetOptions = j === zone.left ? dataSet : { yAxisId: dataSet.yAxisId };
                            postProcessedRanges.push({
                                ...datasetOptions,
                                dataRange: `${sheetPrefix}${zoneToXc({
                                    left: j,
                                    right: j,
                                    top: zone.top,
                                    bottom: zone.bottom,
                                })}`,
                            });
                        }
                    }
                    else if (zone.bottom !== undefined) {
                        for (let j = zone.top; j <= zone.bottom; ++j) {
                            const datasetOptions = j === zone.top ? dataSet : { yAxisId: dataSet.yAxisId };
                            postProcessedRanges.push({
                                ...datasetOptions,
                                dataRange: `${sheetPrefix}${zoneToXc({
                                    left: zone.left,
                                    right: zone.right,
                                    top: j,
                                    bottom: j,
                                })}`,
                            });
                        }
                    }
                }
                else {
                    if (zone.bottom !== undefined) {
                        for (let j = zone.top; j <= zone.bottom; ++j) {
                            const datasetOptions = j === zone.top ? dataSet : { yAxisId: dataSet.yAxisId };
                            postProcessedRanges.push({
                                ...datasetOptions,
                                dataRange: `${sheetPrefix}${zoneToXc({
                                    left: zone.left,
                                    right: zone.right,
                                    top: j,
                                    bottom: j,
                                })}`,
                            });
                        }
                    }
                    else if (zone.right !== undefined) {
                        for (let j = zone.left; j <= zone.right; ++j) {
                            const datasetOptions = j === zone.left ? dataSet : { yAxisId: dataSet.yAxisId };
                            postProcessedRanges.push({
                                ...datasetOptions,
                                dataRange: `${sheetPrefix}${zoneToXc({
                                    left: j,
                                    right: j,
                                    top: zone.top,
                                    bottom: zone.bottom,
                                })}`,
                            });
                        }
                    }
                }
            }
            else {
                postProcessedRanges.push(dataSet);
            }
        }
        return postProcessedRanges;
    }
    getDataSeriesRanges() {
        return this.dataSets;
    }
    /**
     * Change the local labelRange. The model should be updated when the
     * button "confirm" is clicked
     */
    onLabelRangeChanged(ranges) {
        this.labelRange = ranges[0];
        this.state.labelsDispatchResult = this.props.canUpdateChart(this.props.chartId, {
            labelRange: this.labelRange,
        });
    }
    onLabelRangeConfirmed() {
        this.state.labelsDispatchResult = this.props.updateChart(this.props.chartId, {
            labelRange: this.labelRange,
        });
    }
    getLabelRange() {
        return this.labelRange || "";
    }
    onUpdateAggregated(aggregated) {
        this.props.updateChart(this.props.chartId, {
            aggregated,
        });
    }
    calculateHeaderPosition() {
        if (this.isDatasetInvalid || this.isLabelInvalid) {
            return undefined;
        }
        const getters = this.env.model.getters;
        const sheetId = getters.getActiveSheetId();
        const labelRange = createValidRange(getters, sheetId, this.labelRange);
        const dataSets = createDataSets(getters, this.dataSets, sheetId, this.props.definition.dataSetsHaveTitle);
        if (dataSets.length) {
            return this.datasetOrientation === "rows"
                ? dataSets[0].dataRange.zone.left
                : dataSets[0].dataRange.zone.top + 1;
        }
        else if (labelRange) {
            return labelRange.zone.top + 1;
        }
        return undefined;
    }
    get maxNumberOfUsedRanges() {
        return chartRegistry.get(this.props.definition.type).dataSeriesLimit;
    }
    transposeDataSet(dataRanges, datasetOrientation) {
        const getters = this.env.model.getters;
        if (datasetOrientation === undefined) {
            return dataRanges.filter(isDefined).map((dataRange) => ({ dataRange }));
        }
        const zonesBySheetName = {};
        const transposedDatasets = [];
        const figureId = getters.getFigureIdFromChartId(this.props.chartId);
        const figureSheetId = getters.getFigureSheetId(figureId);
        let name = getters.getActiveSheet().name;
        if (figureSheetId) {
            name = getters.getSheet(figureSheetId).name;
        }
        for (const dataRange of dataRanges) {
            if (!dataRange) {
                continue;
            }
            if (!isXcRepresentation(dataRange)) {
                return dataRanges.filter(isDefined).map((dataRange) => ({ dataRange }));
            }
            let { sheetName, xc } = splitReference(dataRange);
            sheetName = sheetName ?? name;
            if (!zonesBySheetName[sheetName]) {
                zonesBySheetName[sheetName] = [];
            }
            zonesBySheetName[sheetName].push(toZone(xc));
        }
        for (const sheetName in zonesBySheetName) {
            const zones = zonesBySheetName[sheetName];
            const contiguousZones = mergeContiguousZones(zones);
            if (datasetOrientation === "columns") {
                for (const zone of contiguousZones) {
                    for (let col = zone.left; col <= zone.right; col++) {
                        const dataRange = `${sheetName === name ? "" : sheetName + "!"}${zoneToXc({
                            ...zone,
                            left: col,
                            right: col,
                        })}`;
                        transposedDatasets.push({ dataRange });
                    }
                }
            }
            else {
                for (const zone of contiguousZones) {
                    for (let row = zone.top; row <= zone.bottom; row++) {
                        const dataRange = `${sheetName === name ? "" : sheetName + "!"}${zoneToXc({
                            ...zone,
                            top: row,
                            bottom: row,
                        })}`;
                        transposedDatasets.push({ dataRange });
                    }
                }
            }
        }
        return transposedDatasets;
    }
}

class BarConfigPanel extends GenericChartConfigPanel {
    static template = "o-spreadsheet-BarConfigPanel";
    get stackedLabel() {
        const definition = this.props.definition;
        return definition.horizontal
            ? this.chartTerms.StackedBarChart
            : this.chartTerms.StackedColumnChart;
    }
    onUpdateStacked(stacked) {
        this.props.updateChart(this.props.chartId, {
            stacked,
        });
    }
}

css /* scss */ `
  .o-badge-selection {
    gap: 1px;
    button.o-button {
      border-radius: 0;
      &.selected {
        color: ${GRAY_900};
        border-color: ${ACTION_COLOR};
        background: ${BADGE_SELECTED_COLOR};
        font-weight: 600;
      }

      &:first-child {
        border-radius: 4px 0 0 4px;
      }
      &:last-child {
        border-radius: 0 4px 4px 0;
      }
    }
  }
`;
class BadgeSelection extends Component {
    static template = "o-spreadsheet.BadgeSelection";
    static props = {
        choices: Array,
        onChange: Function,
        selectedValue: String,
    };
}

css /* scss */ `
  .o-chart-title-designer {
    > span {
      height: 30px;
    }
  }
`;
class ChartTitle extends Component {
    static template = "o-spreadsheet.ChartTitle";
    static components = { Section, TextStyler };
    static props = {
        title: { type: String, optional: true },
        placeholder: { type: String, optional: true },
        updateTitle: Function,
        name: { type: String },
        style: Object,
        defaultStyle: { type: Object, optional: true },
        updateStyle: Function,
    };
    static defaultProps = {
        title: "",
        placeholder: "",
    };
    updateTitle(ev) {
        this.props.updateTitle(ev.target.value);
    }
}

class AxisDesignEditor extends Component {
    static template = "o-spreadsheet-AxisDesignEditor";
    static components = { Section, ChartTitle, BadgeSelection };
    static props = { chartId: String, definition: Object, updateChart: Function, axesList: Array };
    state = useState({ currentAxis: "x" });
    defaultFontSize = CHART_AXIS_TITLE_FONT_SIZE;
    get axisTitleStyle() {
        return this.props.definition.axesDesign?.[this.state.currentAxis]?.title ?? {};
    }
    get badgeAxes() {
        return this.props.axesList.map((axis) => ({ value: axis.id, label: axis.name }));
    }
    updateAxisEditor(ev) {
        this.state.currentAxis = ev.target.value;
    }
    getAxisTitle() {
        const axesDesign = this.props.definition.axesDesign ?? {};
        return axesDesign[this.state.currentAxis]?.title.text || "";
    }
    updateAxisTitle(text) {
        const axesDesign = deepCopy(this.props.definition.axesDesign) ?? {};
        axesDesign[this.state.currentAxis] = {
            ...axesDesign[this.state.currentAxis],
            title: {
                ...axesDesign?.[this.state.currentAxis]?.title,
                text,
            },
        };
        this.props.updateChart(this.props.chartId, { axesDesign });
    }
    updateAxisTitleStyle(style) {
        const axesDesign = deepCopy(this.props.definition.axesDesign) ?? {};
        axesDesign[this.state.currentAxis] = {
            ...axesDesign[this.state.currentAxis],
            title: style,
        };
        this.props.updateChart(this.props.chartId, { axesDesign });
    }
}

const CIRCLE_SVG = /*xml*/ `
<svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'>
  <circle r="2" fill="#FFF"/>
</svg>
`;
css /* scss */ `
  .o-radio {
    input {
      appearance: none;
      -webkit-appearance: none;
      -moz-appearance: none;
      width: 14px;
      height: 14px;
      border: 1px solid ${GRAY_300};
      outline: none;
      border-radius: 8px;

      &:checked {
        background: url("data:image/svg+xml,${encodeURIComponent(CIRCLE_SVG)}");
        background-color: ${ACTION_COLOR};
        border-color: ${ACTION_COLOR};
      }
    }
  }
`;
class RadioSelection extends Component {
    static template = "o-spreadsheet.RadioSelection";
    static props = {
        choices: Array,
        onChange: Function,
        selectedValue: { optional: false },
        name: String,
        direction: { type: String, optional: true },
    };
    static defaultProps = {
        direction: "horizontal",
    };
}

class GeneralDesignEditor extends Component {
    static template = "o-spreadsheet-GeneralDesignEditor";
    static components = {
        RoundColorPicker,
        ChartTitle,
        Section,
        SidePanelCollapsible,
        RadioSelection,
    };
    static props = {
        chartId: String,
        definition: Object,
        updateChart: Function,
        canUpdateChart: Function,
        defaultChartTitleFontSize: { type: Number, optional: true },
        slots: { type: Object, optional: true },
    };
    static defaultProps = {
        defaultChartTitleFontSize: CHART_TITLE_FONT_SIZE,
    };
    state;
    setup() {
        this.state = useState({
            activeTool: "",
        });
    }
    get title() {
        return this.props.definition.title;
    }
    toggleDropdownTool(tool, ev) {
        const isOpen = this.state.activeTool === tool;
        this.state.activeTool = isOpen ? "" : tool;
    }
    updateBackgroundColor(color) {
        this.props.updateChart(this.props.chartId, {
            background: color,
        });
    }
    updateTitle(newTitle) {
        const title = { ...this.title, text: newTitle };
        this.props.updateChart(this.props.chartId, { title });
    }
    updateChartTitleStyle(style) {
        const title = { ...this.title, ...style };
        this.props.updateChart(this.props.chartId, { title });
        this.state.activeTool = "";
    }
}

class ChartHumanizeNumbers extends Component {
    static template = "o-spreadsheet-ChartHumanizeNumbers";
    static components = {
        Checkbox,
    };
    static props = {
        chartId: String,
        definition: Object,
        updateChart: Function,
        canUpdateChart: Function,
    };
    get title() {
        const locale = this.env.model.getters.getLocale();
        const format = formatLargeNumber({ value: 1234567 }, undefined, locale);
        const value = formatValue(1234567, { format, locale });
        return _t("E.g. 1234567 -> %(value)s", { value });
    }
}

class ChartLegend extends Component {
    static template = "o-spreadsheet-ChartLegend";
    static components = {
        Section,
    };
    static props = {
        chartId: String,
        definition: Object,
        updateChart: Function,
        canUpdateChart: Function,
    };
    updateLegendPosition(ev) {
        this.props.updateChart(this.props.chartId, {
            legendPosition: ev.target.value,
        });
    }
}

class SeriesDesignEditor extends Component {
    static template = "o-spreadsheet-SeriesDesignEditor";
    static components = {
        SidePanelCollapsible,
        Section,
        RoundColorPicker,
    };
    static props = {
        chartId: String,
        definition: Object,
        updateChart: Function,
        canUpdateChart: Function,
        slots: { type: Object, optional: true },
    };
    state = useState({ index: 0 });
    getDataSeries() {
        const runtime = this.env.model.getters.getChartRuntime(this.props.chartId);
        if (!runtime || !("chartJsConfig" in runtime)) {
            return [];
        }
        return runtime.chartJsConfig.data.datasets
            .filter((d) => !isTrendLineAxis(d["xAxisID"] ?? ""))
            .map((d) => d.label);
    }
    updateEditedSeries(ev) {
        this.state.index = ev.target.selectedIndex;
    }
    updateDataSeriesColor(color) {
        const dataSets = this.props.definition.dataSets;
        if (!dataSets?.[this.state.index])
            return;
        dataSets[this.state.index] = {
            ...dataSets[this.state.index],
            backgroundColor: color,
        };
        this.props.updateChart(this.props.chartId, { dataSets });
    }
    getDataSeriesColor() {
        const dataSets = this.props.definition.dataSets;
        if (!dataSets?.[this.state.index])
            return "";
        const color = dataSets[this.state.index].backgroundColor;
        return color
            ? toHex(color)
            : getNthColor(this.state.index, getColorsPalette(this.props.definition.dataSets.length));
    }
    updateDataSeriesLabel(ev) {
        const label = ev.target.value;
        const dataSets = this.props.definition.dataSets;
        if (!dataSets?.[this.state.index])
            return;
        dataSets[this.state.index] = {
            ...dataSets[this.state.index],
            label,
        };
        this.props.updateChart(this.props.chartId, { dataSets });
    }
    getDataSeriesLabel() {
        const dataSets = this.props.definition.dataSets;
        return dataSets[this.state.index]?.label || this.getDataSeries()[this.state.index];
    }
}

class SeriesWithAxisDesignEditor extends Component {
    static template = "o-spreadsheet-SeriesWithAxisDesignEditor";
    static components = {
        SeriesDesignEditor,
        Checkbox,
        RadioSelection,
        Section,
        RoundColorPicker,
    };
    static props = {
        chartId: String,
        definition: Object,
        canUpdateChart: Function,
        updateChart: Function,
        slots: { type: Object, optional: true },
    };
    axisChoices = CHART_AXIS_CHOICES;
    updateDataSeriesAxis(index, axis) {
        const dataSets = [...this.props.definition.dataSets];
        if (!dataSets?.[index]) {
            return;
        }
        dataSets[index] = {
            ...dataSets[index],
            yAxisId: axis === "left" ? "y" : "y1",
        };
        this.props.updateChart(this.props.chartId, { dataSets });
    }
    getDataSerieAxis(index) {
        const dataSets = this.props.definition.dataSets;
        if (!dataSets?.[index]) {
            return "left";
        }
        return dataSets[index].yAxisId === "y1" ? "right" : "left";
    }
    get canHaveTwoVerticalAxis() {
        return !("horizontal" in this.props.definition && this.props.definition.horizontal);
    }
    toggleDataTrend(index, display) {
        const dataSets = [...this.props.definition.dataSets];
        if (!dataSets?.[index]) {
            return;
        }
        dataSets[index] = {
            ...dataSets[index],
            trend: {
                type: "polynomial",
                order: 1,
                ...dataSets[index].trend,
                display,
            },
        };
        this.props.updateChart(this.props.chartId, { dataSets });
    }
    getTrendLineConfiguration(index) {
        const dataSets = this.props.definition.dataSets;
        return dataSets?.[index]?.trend;
    }
    getTrendType(config) {
        if (!config) {
            return "";
        }
        return config.type === "polynomial" && config.order === 1 ? "linear" : config.type;
    }
    onChangeTrendType(index, ev) {
        const type = ev.target.value;
        let config;
        switch (type) {
            case "linear":
            case "polynomial":
                config = {
                    type: "polynomial",
                    order: type === "linear" ? 1 : this.getMaxPolynomialDegree(index),
                };
                break;
            case "exponential":
            case "logarithmic":
            case "trailingMovingAverage":
                config = { type };
                break;
            default:
                return;
        }
        this.updateTrendLineValue(index, config);
    }
    getPolynomialDegrees(index) {
        return range(1, this.getMaxPolynomialDegree(index) + 1);
    }
    onChangePolynomialDegree(index, ev) {
        const element = ev.target;
        this.updateTrendLineValue(index, { order: parseInt(element.value) });
    }
    getMaxPolynomialDegree(index) {
        const runtime = this.env.model.getters.getChartRuntime(this.props.chartId);
        return Math.min(10, runtime.chartJsConfig.data.datasets[index].data.length - 1);
    }
    get defaultWindowSize() {
        return DEFAULT_WINDOW_SIZE;
    }
    onChangeMovingAverageWindow(index, ev) {
        const element = ev.target;
        let window = parseInt(element.value) || DEFAULT_WINDOW_SIZE;
        if (window <= 1) {
            window = DEFAULT_WINDOW_SIZE;
        }
        this.updateTrendLineValue(index, { window });
    }
    getDataSeriesColor(index) {
        const dataSets = this.props.definition.dataSets;
        if (!dataSets?.[index])
            return "";
        const color = dataSets[index].backgroundColor;
        return color
            ? toHex(color)
            : getNthColor(index, getColorsPalette(this.props.definition.dataSets.length));
    }
    getTrendLineColor(index) {
        return (this.getTrendLineConfiguration(index)?.color ??
            setColorAlpha(this.getDataSeriesColor(index), 0.5));
    }
    updateTrendLineColor(index, color) {
        this.updateTrendLineValue(index, { color });
    }
    updateTrendLineValue(index, config) {
        const dataSets = [...this.props.definition.dataSets];
        if (!dataSets?.[index]) {
            return;
        }
        dataSets[index] = {
            ...dataSets[index],
            trend: {
                ...dataSets[index].trend,
                ...config,
            },
        };
        this.props.updateChart(this.props.chartId, { dataSets });
    }
}

class ChartShowValues extends Component {
    static template = "o-spreadsheet-ChartShowValues";
    static components = {
        Checkbox,
    };
    static props = {
        chartId: String,
        definition: Object,
        updateChart: Function,
        canUpdateChart: Function,
        defaultValue: { type: Boolean, optional: true },
    };
}

class ChartWithAxisDesignPanel extends Component {
    static template = "o-spreadsheet-ChartWithAxisDesignPanel";
    static components = {
        GeneralDesignEditor,
        SidePanelCollapsible,
        Section,
        AxisDesignEditor,
        SeriesWithAxisDesignEditor,
        ChartLegend,
        ChartShowValues,
        ChartHumanizeNumbers,
    };
    static props = {
        chartId: String,
        definition: Object,
        canUpdateChart: Function,
        updateChart: Function,
    };
    get axesList() {
        const { useLeftAxis, useRightAxis } = getDefinedAxis(this.props.definition);
        const axes = [{ id: "x", name: _t("Horizontal axis") }];
        if (useLeftAxis) {
            axes.push({ id: "y", name: useRightAxis ? _t("Left axis") : _t("Vertical axis") });
        }
        if (useRightAxis) {
            axes.push({ id: "y1", name: useLeftAxis ? _t("Right axis") : _t("Vertical axis") });
        }
        return axes;
    }
}

class GenericZoomableChartDesignPanel extends ChartWithAxisDesignPanel {
    static template = "o-spreadsheet-GenericZoomableChartDesignPanel";
    static components = {
        ...ChartWithAxisDesignPanel.components,
        Checkbox,
    };
    onToggleZoom(zoomable) {
        this.props.updateChart(this.props.chartId, {
            zoomable,
        });
    }
}

class BarChartDesignPanel extends GenericZoomableChartDesignPanel {
    static template = "o-spreadsheet-BarChartDesignPanel";
    get isZoomable() {
        return !this.props.definition.horizontal;
    }
}

class ChartShowDataMarkers extends Component {
    static template = "o-spreadsheet-ChartShowDataMarkers";
    static components = {
        Checkbox,
    };
    static props = {
        chartId: String,
        definition: Object,
        updateChart: Function,
        canUpdateChart: Function,
    };
}

class ComboChartDesignPanel extends GenericZoomableChartDesignPanel {
    static template = "o-spreadsheet-ComboChartDesignPanel";
    static components = {
        ...GenericZoomableChartDesignPanel.components,
        ChartShowDataMarkers,
        RadioSelection,
    };
    seriesTypeChoices = [
        { value: "bar", label: _t("Bar") },
        { value: "line", label: _t("Line") },
    ];
    updateDataSeriesType(index, type) {
        const dataSets = [...this.props.definition.dataSets];
        if (!dataSets?.[index]) {
            return;
        }
        dataSets[index] = {
            ...dataSets[index],
            type,
        };
        this.props.updateChart(this.props.chartId, { dataSets });
    }
    getDataSeriesType(index) {
        const dataSets = this.props.definition.dataSets;
        if (!dataSets?.[index]) {
            return "bar";
        }
        return dataSets[index].type ?? "line";
    }
}

class FunnelChartConfigPanel extends GenericChartConfigPanel {
    getLabelRangeOptions() {
        const definition = this.props.definition;
        return [
            {
                name: "aggregated",
                label: this.chartTerms.AggregatedChart,
                value: definition.aggregated ?? false,
                onChange: this.onUpdateAggregated.bind(this),
            },
            {
                name: "cumulative",
                label: this.chartTerms.CumulativeData,
                value: definition.cumulative ?? false,
                onChange: this.onUpdateCumulative.bind(this),
            },
            {
                name: "dataSetsHaveTitle",
                label: this.dataSetsHaveTitleLabel,
                value: definition.dataSetsHaveTitle,
                onChange: this.onUpdateDataSetsHaveTitle.bind(this),
            },
        ];
    }
    onUpdateCumulative(cumulative) {
        this.props.updateChart(this.props.chartId, {
            cumulative,
        });
    }
}

class FunnelChartDesignPanel extends Component {
    static template = "o-spreadsheet-FunnelChartDesignPanel";
    static components = {
        ChartShowValues,
        GeneralDesignEditor,
        SidePanelCollapsible,
        RoundColorPicker,
        Section,
        ChartHumanizeNumbers,
    };
    static props = {
        chartId: String,
        definition: Object,
        updateChart: Function,
        canUpdateChart: Function,
    };
    getFunnelColorItems() {
        const runtime = this.env.model.getters.getChartRuntime(this.props.chartId);
        const labels = (runtime.chartJsConfig.data.labels || []);
        const colors = getFunnelLabelColors(labels, this.props.definition.funnelColors);
        return labels.map((label, index) => ({
            label: label || _t("Value %s", index + 1),
            color: colors[index],
        }));
    }
    updateFunnelItemColor(index, color) {
        const funnelColors = replaceItemAtIndex(this.props.definition.funnelColors || [], color, index);
        this.props.updateChart(this.props.chartId, { funnelColors });
    }
}

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

css /* scss */ `
  .o-gauge-color-set {
    table {
      table-layout: fixed;
      margin-top: 2%;
      display: table;
      text-align: left;
      font-size: 12px;
      line-height: 18px;
      width: 100%;
      font-size: 12px;
    }

    td {
      height: 30px;
      padding: 6px 0;
    }
    th.o-gauge-color-set-colorPicker {
      width: 8%;
    }
    th.o-gauge-color-set-text {
      width: 25%;
    }
    th.o-gauge-color-set-operator {
      width: 10%;
    }
    th.o-gauge-color-set-value {
      width: 22%;
    }
    th.o-gauge-color-set-type {
      width: 30%;
    }
    input,
    select {
      width: 100%;
      height: 100%;
    }
  }
`;
class GaugeChartDesignPanel extends Component {
    static template = "o-spreadsheet-GaugeChartDesignPanel";
    static components = {
        SidePanelCollapsible,
        Section,
        RoundColorPicker,
        GeneralDesignEditor,
        ChartErrorSection,
        StandaloneComposer,
        ChartHumanizeNumbers,
    };
    static props = {
        chartId: String,
        definition: Object,
        updateChart: Function,
        canUpdateChart: { type: Function, optional: true },
    };
    state;
    setup() {
        this.state = useState({
            sectionRuleCancelledReasons: this.checkSectionRuleFormulasAreValid(this.props.definition.sectionRule),
            sectionRule: deepCopy(this.props.definition.sectionRule),
        });
    }
    get designErrorMessages() {
        const cancelledReasons = [...(this.state.sectionRuleCancelledReasons || [])].filter((reason) => reason !== "NoChanges" /* CommandResult.NoChanges */);
        return cancelledReasons.map((error) => ChartTerms.Errors[error] || ChartTerms.Errors.Unexpected);
    }
    get isRangeMinInvalid() {
        return !!(this.state.sectionRuleCancelledReasons?.includes("EmptyGaugeRangeMin" /* CommandResult.EmptyGaugeRangeMin */) ||
            this.state.sectionRuleCancelledReasons?.includes("GaugeRangeMinNaN" /* CommandResult.GaugeRangeMinNaN */));
    }
    get isRangeMaxInvalid() {
        return !!(this.state.sectionRuleCancelledReasons?.includes("EmptyGaugeRangeMax" /* CommandResult.EmptyGaugeRangeMax */) ||
            this.state.sectionRuleCancelledReasons?.includes("GaugeRangeMaxNaN" /* CommandResult.GaugeRangeMaxNaN */));
    }
    // ---------------------------------------------------------------------------
    // COLOR_SECTION_TEMPLATE
    // ---------------------------------------------------------------------------
    get isLowerInflectionPointInvalid() {
        return !!this.state.sectionRuleCancelledReasons?.includes("GaugeLowerInflectionPointNaN" /* CommandResult.GaugeLowerInflectionPointNaN */);
    }
    get isUpperInflectionPointInvalid() {
        return !!this.state.sectionRuleCancelledReasons?.includes("GaugeUpperInflectionPointNaN" /* CommandResult.GaugeUpperInflectionPointNaN */);
    }
    updateSectionColor(target, color) {
        const sectionRule = deepCopy(this.state.sectionRule);
        sectionRule.colors[target] = color;
        this.updateSectionRule(sectionRule);
    }
    updateSectionRule(sectionRule) {
        this.state.sectionRuleCancelledReasons = [];
        this.state.sectionRuleCancelledReasons.push(...this.checkSectionRuleFormulasAreValid(this.state.sectionRule));
        const dispatchResult = this.props.updateChart(this.props.chartId, {
            sectionRule,
        });
        if (dispatchResult.isSuccessful) {
            this.state.sectionRule = deepCopy(sectionRule);
        }
        else {
            this.state.sectionRuleCancelledReasons.push(...dispatchResult.reasons);
        }
    }
    onConfirmGaugeRange(editedRange, content) {
        this.state.sectionRule = { ...this.state.sectionRule, [editedRange]: content };
        this.updateSectionRule(this.state.sectionRule);
    }
    getGaugeInflectionComposerProps(sectionType) {
        const inflectionPointName = sectionType === "lowerColor" ? "lowerInflectionPoint" : "upperInflectionPoint";
        const inflectionPoint = this.state.sectionRule[inflectionPointName];
        return {
            onConfirm: (str) => {
                this.state.sectionRule = {
                    ...this.state.sectionRule,
                    [inflectionPointName]: { ...inflectionPoint, value: str },
                };
                this.updateSectionRule(this.state.sectionRule);
            },
            composerContent: inflectionPoint.value,
            invalid: sectionType === "lowerColor"
                ? this.isLowerInflectionPointInvalid
                : this.isUpperInflectionPointInvalid,
            defaultRangeSheetId: this.sheetId,
            class: inflectionPointName,
            placeholder: _t("Value"),
            title: _t("Value or formula"),
        };
    }
    checkSectionRuleFormulasAreValid(sectionRule) {
        const reasons = [];
        if (!this.valueIsValidNumber(sectionRule.rangeMin)) {
            reasons.push("GaugeRangeMinNaN" /* CommandResult.GaugeRangeMinNaN */);
        }
        if (!this.valueIsValidNumber(sectionRule.rangeMax)) {
            reasons.push("GaugeRangeMaxNaN" /* CommandResult.GaugeRangeMaxNaN */);
        }
        if (!this.valueIsValidNumber(sectionRule.lowerInflectionPoint.value)) {
            reasons.push("GaugeLowerInflectionPointNaN" /* CommandResult.GaugeLowerInflectionPointNaN */);
        }
        if (!this.valueIsValidNumber(sectionRule.upperInflectionPoint.value)) {
            reasons.push("GaugeUpperInflectionPointNaN" /* CommandResult.GaugeUpperInflectionPointNaN */);
        }
        return reasons;
    }
    valueIsValidNumber(value) {
        const locale = this.env.model.getters.getLocale();
        if (!value.startsWith("=")) {
            return tryToNumber(value, locale) !== undefined;
        }
        const evaluatedValue = this.env.model.getters.evaluateFormula(this.sheetId, value);
        if (isMultipleElementMatrix(evaluatedValue)) {
            return false;
        }
        return tryToNumber(toScalar(evaluatedValue), locale) !== undefined;
    }
    get sheetId() {
        const chart = this.env.model.getters.getChart(this.props.chartId);
        if (!chart) {
            throw new Error("Chart not found with id " + this.props.chartId);
        }
        return chart.sheetId;
    }
}

class GeoChartRegionSelectSection extends Component {
    static template = "o-spreadsheet-GeoChartRegionSelectSection";
    static components = { Section };
    static props = {
        chartId: String,
        definition: Object,
        updateChart: Function,
    };
    updateSelectedRegion(ev) {
        const value = ev.target.value;
        this.props.updateChart(this.props.chartId, { region: value });
    }
    get availableRegions() {
        return this.env.model.getters.getGeoChartAvailableRegions();
    }
    get selectedRegion() {
        return this.props.definition.region || this.availableRegions[0]?.id;
    }
}

class GeoChartConfigPanel extends GenericChartConfigPanel {
    static template = "o-spreadsheet-GeoChartConfigPanel";
    static components = {
        ...GenericChartConfigPanel.components,
        GeoChartRegionSelectSection,
    };
    get dataRanges() {
        return this.getDataSeriesRanges();
    }
    get disabledRanges() {
        return this.props.definition.dataSets.map((ds, i) => i > 0);
    }
    getLabelRangeOptions() {
        return [
            {
                name: "dataSetsHaveTitle",
                label: this.dataSetsHaveTitleLabel,
                value: this.props.definition.dataSetsHaveTitle,
                onChange: this.onUpdateDataSetsHaveTitle.bind(this),
            },
        ];
    }
}

const DEFAULT_CUSTOM_COLOR_SCALE = {
    minColor: "#FFF5EB",
    midColor: "#FD8D3C",
    maxColor: "#7F2704",
};
class GeoChartDesignPanel extends ChartWithAxisDesignPanel {
    static template = "o-spreadsheet-GeoChartDesignPanel";
    static components = { ...ChartWithAxisDesignPanel.components, RoundColorPicker };
    colorScalesChoices = ChartTerms.GeoChart.ColorScales;
    updateColorScaleType(ev) {
        const value = ev.target.value;
        value === "custom"
            ? this.updateColorScale(DEFAULT_CUSTOM_COLOR_SCALE)
            : this.updateColorScale(value);
    }
    updateColorScale(colorScale) {
        this.props.updateChart(this.props.chartId, { colorScale });
    }
    updateMissingValueColor(color) {
        this.props.updateChart(this.props.chartId, { missingValueColor: color });
    }
    updateLegendPosition(ev) {
        const value = ev.target.value;
        this.props.updateChart(this.props.chartId, { legendPosition: value });
    }
    get selectedColorScale() {
        return typeof this.props.definition.colorScale === "object"
            ? "custom"
            : this.props.definition.colorScale || "oranges";
    }
    get selectedMissingValueColor() {
        return this.props.definition.missingValueColor || "#ffffff";
    }
    get customColorScale() {
        if (typeof this.props.definition.colorScale === "object") {
            return this.props.definition.colorScale;
        }
        return undefined;
    }
    getCustomColorScaleColor(color) {
        return this.customColorScale?.[color] ?? "";
    }
    setCustomColorScaleColor(colorType, color) {
        if (!color && colorType !== "midColor") {
            color = "#fff";
        }
        const customColorScale = this.customColorScale;
        if (!customColorScale) {
            return;
        }
        this.updateColorScale({ ...customColorScale, [colorType]: color });
    }
}

class HierarchicalChartConfigPanel extends GenericChartConfigPanel {
    static template = "o-spreadsheet-HierarchicalChartConfigPanel";
    static components = { ...GenericChartConfigPanel.components };
    getLabelRangeOptions() {
        return [
            {
                name: "dataSetsHaveTitle",
                label: this.dataSetsHaveTitleLabel,
                value: this.props.definition.dataSetsHaveTitle,
                onChange: this.onUpdateDataSetsHaveTitle.bind(this),
            },
        ];
    }
}

class LineConfigPanel extends GenericChartConfigPanel {
    static template = "o-spreadsheet-LineConfigPanel";
    get canTreatLabelsAsText() {
        const chart = this.env.model.getters.getChart(this.props.chartId);
        if (chart && chart instanceof LineChart) {
            return canChartParseLabels(chart.getDefinition(), chart.dataSets, chart.labelRange, this.env.model.getters);
        }
        return false;
    }
    get stackedLabel() {
        const definition = this.props.definition;
        return definition.fillArea
            ? this.chartTerms.StackedAreaChart
            : this.chartTerms.StackedLineChart;
    }
    getLabelRangeOptions() {
        const options = super.getLabelRangeOptions();
        if (this.canTreatLabelsAsText) {
            options.push({
                name: "labelsAsText",
                value: this.props.definition.labelsAsText,
                label: this.chartTerms.TreatLabelsAsText,
                onChange: this.onUpdateLabelsAsText.bind(this),
            });
        }
        return options;
    }
    onUpdateLabelsAsText(labelsAsText) {
        this.props.updateChart(this.props.chartId, {
            labelsAsText,
        });
    }
    onUpdateStacked(stacked) {
        this.props.updateChart(this.props.chartId, {
            stacked,
        });
    }
    onUpdateCumulative(cumulative) {
        this.props.updateChart(this.props.chartId, {
            cumulative,
        });
    }
}

class LineChartDesignPanel extends GenericZoomableChartDesignPanel {
    static template = "o-spreadsheet-LineChartDesignPanel";
    static components = {
        ...GenericZoomableChartDesignPanel.components,
        ChartShowDataMarkers,
    };
}

class PieHoleSize extends Component {
    static template = "o-spreadsheet.PieHoleSize";
    static components = { Section };
    static props = { onValueChange: Function, value: Number };
    // Very short debounce to prevent up/down arrow on number input to spam the onChange
    debouncedOnChange = debounce(this.onChange.bind(this), 100);
    onChange(value) {
        if (!isNaN(Number(value))) {
            this.props.onValueChange(clip(Number(value), 0, 95));
        }
    }
}

class PieChartDesignPanel extends Component {
    static template = "o-spreadsheet-PieChartDesignPanel";
    static components = {
        GeneralDesignEditor,
        Section,
        ChartLegend,
        ChartShowValues,
        PieHoleSize,
        Checkbox,
        ChartHumanizeNumbers,
    };
    static props = {
        chartId: String,
        definition: Object,
        updateChart: Function,
        canUpdateChart: { type: Function, optional: true },
    };
    onPieHoleSizeChange(pieHolePercentage) {
        this.props.updateChart(this.props.chartId, {
            ...this.props.definition,
            pieHolePercentage,
        });
    }
    get defaultHoleSize() {
        return DEFAULT_DOUGHNUT_CHART_HOLE_SIZE;
    }
}

class RadarChartDesignPanel extends Component {
    static template = "o-spreadsheet-RadarChartDesignPanel";
    static components = {
        GeneralDesignEditor,
        SeriesDesignEditor,
        Section,
        ChartLegend,
        ChartShowValues,
        ChartShowDataMarkers,
        Checkbox,
        ChartHumanizeNumbers,
    };
    static props = {
        chartId: String,
        definition: Object,
        canUpdateChart: Function,
        updateChart: Function,
    };
}

class ScatterConfigPanel extends GenericChartConfigPanel {
    static template = "o-spreadsheet-ScatterConfigPanel";
    get canTreatLabelsAsText() {
        const chart = this.env.model.getters.getChart(this.props.chartId);
        if (chart && chart instanceof ScatterChart) {
            return canChartParseLabels(chart.getDefinition(), chart.dataSets, chart.labelRange, this.env.model.getters);
        }
        return false;
    }
    onUpdateLabelsAsText(labelsAsText) {
        this.props.updateChart(this.props.chartId, {
            labelsAsText,
        });
    }
    getLabelRangeOptions() {
        const options = super.getLabelRangeOptions();
        if (this.canTreatLabelsAsText) {
            options.push({
                name: "labelsAsText",
                value: this.props.definition.labelsAsText,
                label: this.chartTerms.TreatLabelsAsText,
                onChange: this.onUpdateLabelsAsText.bind(this),
            });
        }
        return options;
    }
}

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

class ScorecardChartDesignPanel extends Component {
    static template = "o-spreadsheet-ScorecardChartDesignPanel";
    static components = {
        GeneralDesignEditor,
        RoundColorPicker,
        SidePanelCollapsible,
        Section,
        Checkbox,
        ChartTitle,
        ChartHumanizeNumbers,
    };
    static props = {
        chartId: String,
        definition: Object,
        updateChart: Function,
        canUpdateChart: { type: Function, optional: true },
    };
    get colorsSectionTitle() {
        return this.props.definition.baselineMode === "progress"
            ? _t("Progress bar colors")
            : _t("Baseline colors");
    }
    get defaultScorecardTitleFontSize() {
        return SCORECARD_CHART_TITLE_FONT_SIZE;
    }
    translate(term) {
        return this.env.model.getters.dynamicTranslate(term);
    }
    setColor(color, colorPickerId) {
        switch (colorPickerId) {
            case "backgroundColor":
                this.props.updateChart(this.props.chartId, { background: color });
                break;
            case "baselineColorDown":
                this.props.updateChart(this.props.chartId, { baselineColorDown: color });
                break;
            case "baselineColorUp":
                this.props.updateChart(this.props.chartId, { baselineColorUp: color });
                break;
        }
    }
    get keyStyle() {
        return {
            align: "center",
            fontSize: DEFAULT_SCORECARD_KEY_VALUE_FONT_SIZE,
            ...this.props.definition.keyDescr,
        };
    }
    get baselineStyle() {
        return {
            align: "center",
            fontSize: DEFAULT_SCORECARD_BASELINE_FONT_SIZE,
            ...this.props.definition.baselineDescr,
        };
    }
    setKeyText(text) {
        this.props.updateChart(this.props.chartId, {
            keyDescr: { ...this.props.definition.keyDescr, text },
        });
    }
    updateKeyStyle(style) {
        const keyDescr = { ...this.keyStyle, ...style };
        this.props.updateChart(this.props.chartId, { keyDescr });
    }
    setBaselineText(text) {
        this.props.updateChart(this.props.chartId, {
            baselineDescr: { ...this.props.definition.baselineDescr, text },
        });
    }
    updateBaselineStyle(style) {
        const baselineDescr = { ...this.baselineStyle, ...style };
        this.props.updateChart(this.props.chartId, { baselineDescr });
    }
}

class SunburstChartDesignPanel extends Component {
    static template = "o-spreadsheet-SunburstChartDesignPanel";
    static components = {
        GeneralDesignEditor,
        Section,
        SidePanelCollapsible,
        ChartShowValues,
        Checkbox,
        TextStyler,
        RoundColorPicker,
        ChartLegend,
        PieHoleSize,
        ChartHumanizeNumbers,
    };
    static props = {
        chartId: String,
        definition: Object,
        updateChart: Function,
        canUpdateChart: { type: Function, optional: true },
    };
    defaults = SunburstChartDefaults;
    get showValues() {
        return this.props.definition.showValues ?? SunburstChartDefaults.showValues;
    }
    get showLabels() {
        return this.props.definition.showLabels ?? SunburstChartDefaults.showLabels;
    }
    get groupColors() {
        const chartId = this.props.chartId;
        const runtime = this.env.model.getters.getChartRuntime(chartId);
        const dataset = runtime.chartJsConfig.data.datasets[0];
        return dataset?.groupColors || [];
    }
    onGroupColorChanged(index, color) {
        const colors = deepCopy(this.props.definition.groupColors) ?? [];
        colors[index] = color;
        this.props.updateChart(this.props.chartId, { groupColors: colors });
    }
    onPieHoleSizeChange(pieHolePercentage) {
        this.props.updateChart(this.props.chartId, {
            ...this.props.definition,
            pieHolePercentage,
        });
    }
}

class TreeMapCategoryColors extends Component {
    static template = "o-spreadsheet-TreeMapCategoryColors";
    static components = {
        Checkbox,
        RoundColorPicker,
    };
    static props = {
        chartId: String,
        definition: Object,
        onColorChanged: Function,
    };
    get coloringOptions() {
        const coloringOptions = this.props.definition.coloringOptions ?? TreeMapChartDefaults.coloringOptions;
        if (coloringOptions.type !== "categoryColor") {
            throw new Error("Coloring options is not solid color");
        }
        return coloringOptions;
    }
    getTreeGroupAndColors() {
        const runtime = this.env.model.getters.getChartRuntime(this.props.chartId);
        const config = runtime.chartJsConfig;
        return config.data.datasets[0]?.groupColors || [];
    }
    onGroupColorChanged(index, color) {
        const coloringOptions = deepCopy(this.coloringOptions);
        coloringOptions.colors[index] = color || undefined; // color picker returns empty string for no color
        this.props.onColorChanged(coloringOptions);
    }
    useValueBasedGradient(useValueBasedGradient) {
        if (this.coloringOptions.type !== "categoryColor") {
            throw new Error("Coloring options is not solid color");
        }
        this.props.onColorChanged({ ...this.coloringOptions, useValueBasedGradient });
    }
}

class TreeMapColorScale extends Component {
    static template = "o-spreadsheet-TreeMapColorScale";
    static components = { RoundColorPicker };
    static props = {
        chartId: String,
        definition: Object,
        onColorChanged: Function,
    };
    get coloringOptions() {
        const coloringOptions = this.props.definition.coloringOptions ?? TreeMapChartDefaults.coloringOptions;
        if (coloringOptions.type !== "colorScale") {
            throw new Error("Coloring options is not a color scale");
        }
        return coloringOptions;
    }
    setColorScaleColor(point, color) {
        this.props.onColorChanged({ ...this.coloringOptions, [point]: color });
    }
}

const DEFAULT_COLOR_SCALE = {
    type: "colorScale",
    minColor: "#FFF5EB",
    midColor: "#FD8D3C",
    maxColor: "#7F2704",
};
const DEFAULT_SOLID_COLOR = {
    type: "categoryColor",
    colors: [],
    useValueBasedGradient: true,
};
class TreeMapChartDesignPanel extends Component {
    static template = "o-spreadsheet-TreeMapChartDesignPanel";
    static components = {
        GeneralDesignEditor,
        Section,
        SidePanelCollapsible,
        ChartShowValues,
        Checkbox,
        TextStyler,
        RoundColorPicker,
        BadgeSelection,
        TreeMapCategoryColors,
        TreeMapColorScale,
        ChartHumanizeNumbers,
    };
    static props = {
        chartId: String,
        definition: Object,
        updateChart: Function,
        canUpdateChart: { type: Function, optional: true },
    };
    savedColors = {
        categoryColors: DEFAULT_SOLID_COLOR,
        colorScale: DEFAULT_COLOR_SCALE,
    };
    defaults = TreeMapChartDefaults;
    get showHeaders() {
        return this.props.definition.showHeaders ?? TreeMapChartDefaults.showHeaders;
    }
    get showValues() {
        return this.props.definition.showValues ?? TreeMapChartDefaults.showValues;
    }
    get showLabels() {
        return this.props.definition.showLabels ?? TreeMapChartDefaults.showLabels;
    }
    get coloringOptions() {
        return this.props.definition.coloringOptions ?? TreeMapChartDefaults.coloringOptions;
    }
    changeColoringOption(option) {
        const coloringOptions = option === "categoryColor" ? this.savedColors.categoryColors : this.savedColors.colorScale;
        this.props.updateChart(this.props.chartId, { coloringOptions });
    }
    onCategoryColorChange(coloringOptions) {
        this.savedColors.categoryColors = coloringOptions;
        this.props.updateChart(this.props.chartId, { coloringOptions });
    }
    onColorScaleChange(coloringOptions) {
        this.savedColors.colorScale = coloringOptions;
        this.props.updateChart(this.props.chartId, { coloringOptions });
    }
    get coloringOptionChoices() {
        return [
            { label: _t("Category color"), value: "categoryColor" },
            { label: _t("Color scale"), value: "colorScale" },
        ];
    }
}

class WaterfallChartDesignPanel extends Component {
    static template = "o-spreadsheet-WaterfallChartDesignPanel";
    static components = {
        GeneralDesignEditor,
        ChartShowValues,
        Checkbox,
        SidePanelCollapsible,
        Section,
        RoundColorPicker,
        AxisDesignEditor,
        RadioSelection,
        ChartLegend,
        ChartHumanizeNumbers,
    };
    static props = {
        chartId: String,
        definition: Object,
        updateChart: Function,
        canUpdateChart: { type: Function, optional: true },
    };
    axisChoices = CHART_AXIS_CHOICES;
    onUpdateShowSubTotals(showSubTotals) {
        this.props.updateChart(this.props.chartId, { showSubTotals });
    }
    onUpdateShowConnectorLines(showConnectorLines) {
        this.props.updateChart(this.props.chartId, { showConnectorLines });
    }
    onUpdateFirstValueAsSubtotal(firstValueAsSubtotal) {
        this.props.updateChart(this.props.chartId, { firstValueAsSubtotal });
    }
    updateColor(colorName, color) {
        this.props.updateChart(this.props.chartId, { [colorName]: color });
    }
    get axesList() {
        return [
            { id: "x", name: _t("Horizontal axis") },
            { id: "y", name: _t("Vertical axis") },
        ];
    }
    get positiveValuesColor() {
        return (this.props.definition.positiveValuesColor ||
            CHART_WATERFALL_POSITIVE_COLOR);
    }
    get negativeValuesColor() {
        return (this.props.definition.negativeValuesColor ||
            CHART_WATERFALL_NEGATIVE_COLOR);
    }
    get subTotalValuesColor() {
        return (this.props.definition.subTotalValuesColor ||
            CHART_WATERFALL_SUBTOTAL_COLOR);
    }
    updateVerticalAxisPosition(value) {
        this.props.updateChart(this.props.chartId, {
            verticalAxisPosition: value,
        });
    }
    onToggleZoom(zoomable) {
        this.props.updateChart(this.props.chartId, {
            zoomable,
        });
    }
}

const chartSidePanelComponentRegistry = new Registry();
chartSidePanelComponentRegistry
    .add("line", {
    configuration: LineConfigPanel,
    design: LineChartDesignPanel,
})
    .add("scatter", {
    configuration: ScatterConfigPanel,
    design: ChartWithAxisDesignPanel,
})
    .add("bar", {
    configuration: BarConfigPanel,
    design: BarChartDesignPanel,
})
    .add("combo", {
    configuration: GenericChartConfigPanel,
    design: ComboChartDesignPanel,
})
    .add("pie", {
    configuration: GenericChartConfigPanel,
    design: PieChartDesignPanel,
})
    .add("gauge", {
    configuration: GaugeChartConfigPanel,
    design: GaugeChartDesignPanel,
})
    .add("scorecard", {
    configuration: ScorecardChartConfigPanel,
    design: ScorecardChartDesignPanel,
})
    .add("waterfall", {
    configuration: GenericChartConfigPanel,
    design: WaterfallChartDesignPanel,
})
    .add("pyramid", {
    configuration: GenericChartConfigPanel,
    design: ChartWithAxisDesignPanel,
})
    .add("radar", {
    configuration: GenericChartConfigPanel,
    design: RadarChartDesignPanel,
})
    .add("sunburst", {
    configuration: HierarchicalChartConfigPanel,
    design: SunburstChartDesignPanel,
})
    .add("geo", {
    configuration: GeoChartConfigPanel,
    design: GeoChartDesignPanel,
})
    .add("funnel", {
    configuration: FunnelChartConfigPanel,
    design: FunnelChartDesignPanel,
})
    .add("treemap", {
    configuration: HierarchicalChartConfigPanel,
    design: TreeMapChartDesignPanel,
});

css /* scss */ `
  .o-section .o-input.o-type-selector {
    height: 30px;
    padding-left: 35px;
    padding-top: 5px;
  }
  .o-type-selector-preview {
    left: 5px;
    top: 3px;
    .o-chart-preview {
      width: 24px;
      height: 24px;
    }
  }

  .o-popover .o-chart-select-popover {
    background: #fff;
    .o-chart-type-item {
      cursor: pointer;
      padding: 3px 6px;
      margin: 1px 2px;
      &.selected,
      &:hover {
        border: 1px solid ${ACTION_COLOR};
        background: ${BADGE_SELECTED_COLOR};
        padding: 2px 5px;
      }
      .o-chart-preview {
        width: 48px;
        height: 48px;
      }
    }
  }
`;
class ChartTypePicker extends Component {
    static template = "o-spreadsheet-ChartTypePicker";
    static components = { Section, Popover };
    static props = { chartId: String, chartPanelStore: Object };
    categories = chartCategories;
    chartTypeByCategories = {};
    popoverRef = useRef("popoverRef");
    selectRef = useRef("selectRef");
    state = useState({ popoverProps: undefined, popoverStyle: "" });
    setup() {
        useExternalListener(window, "pointerdown", this.onExternalClick, { capture: true });
        for (const subtypeProperties of chartSubtypeRegistry.getAll()) {
            if (this.chartTypeByCategories[subtypeProperties.category]) {
                this.chartTypeByCategories[subtypeProperties.category].push(subtypeProperties);
            }
            else {
                this.chartTypeByCategories[subtypeProperties.category] = [subtypeProperties];
            }
        }
    }
    onExternalClick(ev) {
        if (isChildEvent(this.popoverRef.el?.parentElement, ev) ||
            isChildEvent(this.selectRef.el, ev)) {
            return;
        }
        this.closePopover();
    }
    onTypeChange(type) {
        this.props.chartPanelStore.changeChartType(this.props.chartId, type);
        this.closePopover();
    }
    getChartDefinition(chartId) {
        return this.env.model.getters.getChartDefinition(chartId);
    }
    getSelectedChartSubtypeProperties() {
        const definition = this.getChartDefinition(this.props.chartId);
        const matchedChart = chartSubtypeRegistry
            .getAll()
            .find((c) => c.matcher?.(definition) || false);
        return matchedChart || chartSubtypeRegistry.get(definition.type);
    }
    onPointerDown(ev) {
        if (this.state.popoverProps) {
            this.closePopover();
            return;
        }
        const target = ev.currentTarget;
        const { bottom, right, width } = target.getBoundingClientRect();
        this.state.popoverProps = {
            anchorRect: { x: right, y: bottom, width: 0, height: 0 },
            positioning: "top-right",
            verticalOffset: 0,
        };
        this.state.popoverStyle = cssPropertiesToCss({ width: `${width}px` });
    }
    closePopover() {
        this.state.popoverProps = undefined;
    }
}

class MainChartPanelStore extends SpreadsheetStore {
    mutators = ["activatePanel", "changeChartType"];
    panel = "configuration";
    creationContexts = {};
    activatePanel(panel) {
        this.panel = panel;
    }
    changeChartType(chartId, newDisplayType) {
        const currentCreationContext = this.getters.getContextCreationChart(chartId);
        const savedCreationContext = this.creationContexts[chartId] || {};
        let newRanges = currentCreationContext?.range;
        if (newRanges?.every((range, i) => deepEquals(range, savedCreationContext.range?.[i]))) {
            newRanges = Object.assign([], savedCreationContext.range, currentCreationContext?.range);
        }
        this.creationContexts[chartId] = {
            ...savedCreationContext,
            ...currentCreationContext,
            range: newRanges,
        };
        const figureId = this.getters.getFigureIdFromChartId(chartId);
        const sheetId = this.getters.getFigureSheetId(figureId);
        if (!sheetId) {
            return;
        }
        const definition = this.getChartDefinitionFromContextCreation(chartId, newDisplayType);
        this.model.dispatch("UPDATE_CHART", {
            definition,
            chartId,
            figureId,
            sheetId,
        });
    }
    getChartDefinitionFromContextCreation(chartId, newDisplayType) {
        const newChartInfo = chartSubtypeRegistry.get(newDisplayType);
        const ChartClass = chartRegistry.get(newChartInfo.chartType);
        return {
            ...ChartClass.getChartDefinitionFromContextCreation(this.creationContexts[chartId]),
            ...newChartInfo.subtypeDefinition,
        };
    }
}

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 ${GRAY_300};

        &.inactive {
          color: ${TEXT_BODY};
          background-color: ${GRAY_100};
          border-bottom: 1px solid ${GRAY_300};
        }

        &:not(.inactive) {
          color: ${TEXT_HEADING};
          border-bottom: 1px solid #fff;
        }

        .fa {
          margin-right: 4px;
        }
      }
      .o-panel-element:last-child {
        border-right: none;
      }
    }
  }
`;
class ChartPanel extends Component {
    static template = "o-spreadsheet-ChartPanel";
    static components = { Section, ChartTypePicker };
    static props = { onCloseSidePanel: Function, chartId: String };
    panelContentRef;
    scrollPositions = {
        configuration: 0,
        design: 0,
    };
    store;
    get chartId() {
        return this.props.chartId;
    }
    setup() {
        this.store = useLocalStore(MainChartPanelStore);
        this.panelContentRef = useRef("panelContent");
        useEffect(() => {
            const el = this.panelContentRef.el;
            const activePanel = this.store.panel;
            if (el) {
                el.scrollTop = this.scrollPositions[activePanel];
            }
        }, () => [this.store.panel]);
    }
    switchPanel(panel) {
        const el = this.panelContentRef.el;
        if (el) {
            this.scrollPositions[this.store.panel] = el.scrollTop;
        }
        this.store.activatePanel(panel);
    }
    updateChart(chartId, updateDefinition) {
        const figureId = this.env.model.getters.getFigureIdFromChartId(chartId);
        if (chartId !== this.chartId) {
            return;
        }
        const definition = {
            ...this.getChartDefinition(this.chartId),
            ...updateDefinition,
        };
        return this.env.model.dispatch("UPDATE_CHART", {
            definition,
            chartId,
            figureId,
            sheetId: this.env.model.getters.getFigureSheetId(figureId),
        });
    }
    canUpdateChart(chartId, updateDefinition) {
        const figureId = this.env.model.getters.getFigureIdFromChartId(chartId);
        if (chartId !== this.chartId || !this.env.model.getters.isChartDefined(chartId)) {
            return;
        }
        const definition = {
            ...this.getChartDefinition(this.chartId),
            ...updateDefinition,
        };
        return this.env.model.canDispatch("UPDATE_CHART", {
            definition,
            chartId,
            figureId,
            sheetId: this.env.model.getters.getFigureSheetId(figureId),
        });
    }
    onTypeChange(type) {
        if (!this.chartId) {
            return;
        }
        this.store.changeChartType(this.chartId, type);
    }
    get chartPanel() {
        if (!this.chartId) {
            throw new Error("Chart not defined.");
        }
        const type = this.env.model.getters.getChartType(this.chartId);
        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(chartId) {
        return this.env.model.getters.getChartDefinition(chartId);
    }
}

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: 0 6px;
  }
  .o-icon-picker-item {
    cursor: pointer;
    &:hover {
      background-color: ${BADGE_SELECTED_COLOR};
      outline: ${ACTION_COLOR} solid 1px;
    }
  }
`;
class IconPicker extends Component {
    static template = "o-spreadsheet-IconPicker";
    static props = {
        onIconPicked: Function,
    };
    icons = ICONS;
    iconSets = ICON_SETS;
    onIconClick(icon) {
        if (icon) {
            this.props.onIconPicked(icon);
        }
    }
}

function useHighlightsOnHover(ref, highlightProvider) {
    const hoverState = useHoveredElement(ref);
    useHighlights({
        get highlights() {
            return hoverState.hovered ? highlightProvider.highlights : [];
        },
    });
}
function useHighlights(highlightProvider) {
    const stores = useStoreProvider();
    const store = useLocalStore(HighlightStore);
    onMounted(() => {
        store.register(highlightProvider);
    });
    let currentHighlights = highlightProvider.highlights;
    useEffect((highlights) => {
        if (!deepEquals(highlights, currentHighlights)) {
            currentHighlights = highlights;
            stores.trigger("store-updated");
        }
    }, () => [highlightProvider.highlights]);
}

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

    border-bottom: 1px solid ${GRAY_300};
    height: 80px;
    padding: 10px;
    box-sizing: border-box;
    position: relative;
    cursor: pointer;
    &:hover,
    &.o-cf-dragging {
      background-color: ${GRAY_200};
    }

    &:not(:hover) .o-cf-delete-button {
      display: none;
    }
    .o-cf-preview-icon {
      border: 1px solid ${GRAY_300};
      background-color: #fff;
      height: 50px;
      width: 50px;
      .o-icon {
        width: ${CF_ICON_EDGE_LENGTH}px;
        height: ${CF_ICON_EDGE_LENGTH}px;
      }
    }
    .o-cf-preview-description {
      .o-cf-preview-description-rule {
        margin-bottom: 4px;
        max-height: 2.8em;
        line-height: 1.4em;
      }
      .o-cf-preview-range {
        font-size: 12px;
      }
    }
    &:not(:hover):not(.o-cf-dragging) .o-cf-drag-handle {
      display: none !important;
    }
    .o-cf-drag-handle {
      left: 2px;
      cursor: move;
      .o-icon {
        width: 6px;
        height: 30px;
      }
    }

    .o-icon.arrow-down {
      color: #e06666;
    }
    .o-icon.arrow-up {
      color: #6aa84f;
    }
  }
`;
class ConditionalFormatPreview extends Component {
    static template = "o-spreadsheet-ConditionalFormatPreview";
    icons = ICONS;
    ref = useRef("cfPreview");
    setup() {
        useHighlightsOnHover(this.ref, this);
    }
    getPreviewImageStyle() {
        const rule = this.props.conditionalFormat.rule;
        if (rule.type === "CellIsRule") {
            return cssPropertiesToCss(cellStyleToCss(rule.style));
        }
        else if (rule.type === "ColorScaleRule") {
            const minColor = colorNumberToHex(rule.minimum.color);
            const midColor = rule.midpoint ? colorNumberToHex(rule.midpoint.color) : null;
            const maxColor = colorNumberToHex(rule.maximum.color);
            const baseString = "background-image: linear-gradient(to right, ";
            return midColor
                ? baseString + minColor + ", " + midColor + ", " + maxColor + ")"
                : baseString + minColor + ", " + maxColor + ")";
        }
        else if (rule.type === "DataBarRule") {
            const color = colorNumberToHex(rule.color);
            return `background-image: linear-gradient(to right, ${color} 50%, white 50%)`;
        }
        return "";
    }
    getDescription() {
        const cf = this.props.conditionalFormat;
        switch (cf.rule.type) {
            case "CellIsRule":
                return criterionEvaluatorRegistry
                    .get(cf.rule.operator)
                    .getPreview({ ...cf.rule, type: cf.rule.operator }, this.env.model.getters);
            case "ColorScaleRule":
                return CfTerms.ColorScale;
            case "IconSetRule":
                return CfTerms.IconSet;
            case "DataBarRule":
                return CfTerms.DataBar;
        }
    }
    deleteConditionalFormat() {
        this.env.model.dispatch("REMOVE_CONDITIONAL_FORMAT", {
            id: this.props.conditionalFormat.id,
            sheetId: this.env.model.getters.getActiveSheetId(),
        });
    }
    onMouseDown(event) {
        this.props.onMouseDown(event);
    }
    get highlights() {
        const sheetId = this.env.model.getters.getActiveSheetId();
        return this.props.conditionalFormat.ranges.map((range) => ({
            range: this.env.model.getters.getRangeFromSheetXC(sheetId, range),
            color: HIGHLIGHT_COLOR,
            fillAlpha: 0.06,
        }));
    }
}
ConditionalFormatPreview.props = {
    conditionalFormat: Object,
    onPreviewClick: Function,
    onMouseDown: Function,
    class: String,
};

class ConditionalFormatPreviewList extends Component {
    static template = "o-spreadsheet-ConditionalFormatPreviewList";
    static props = {
        conditionalFormats: Array,
        onPreviewClick: Function,
        onAddConditionalFormat: Function,
    };
    static components = { ConditionalFormatPreview };
    icons = ICONS;
    dragAndDrop = useDragAndDropListItems();
    cfListRef = useRef("cfList");
    setup() {
        onWillUpdateProps((nextProps) => {
            if (!deepEquals(this.props.conditionalFormats, nextProps.conditionalFormats)) {
                this.dragAndDrop.cancel();
            }
        });
    }
    getPreviewDivStyle(cf) {
        return this.dragAndDrop.itemsStyle[cf.id] || "";
    }
    onPreviewMouseDown(cf, event) {
        if (event.button !== 0)
            return;
        const previewRects = Array.from(this.cfListRef.el.children).map((previewEl) => getBoundingRectAsPOJO(previewEl));
        const items = this.props.conditionalFormats.map((cf, index) => ({
            id: cf.id,
            size: previewRects[index].height,
            position: previewRects[index].y,
        }));
        this.dragAndDrop.start("vertical", {
            draggedItemId: cf.id,
            initialMousePosition: event.clientY,
            items: items,
            scrollableContainerEl: this.cfListRef.el,
            onDragEnd: (cfId, finalIndex) => this.onDragEnd(cfId, finalIndex),
        });
    }
    onDragEnd(cfId, finalIndex) {
        const originalIndex = this.props.conditionalFormats.findIndex((sheet) => sheet.id === cfId);
        const delta = originalIndex - finalIndex;
        if (delta !== 0) {
            this.env.model.dispatch("CHANGE_CONDITIONAL_FORMAT_PRIORITY", {
                cfId,
                delta,
                sheetId: this.env.model.getters.getActiveSheetId(),
            });
        }
    }
}

css /* scss */ `
  .o-cf-ruleEditor {
    .o-cf-preview-display {
      border: 1px solid ${GRAY_300};
      padding: 10px;
    }

    .o-cf-cell-is-rule {
      .o-divider {
        border-right: 1px solid ${GRAY_300};
        margin: 4px 6px;
      }
    }
    .o-cf-color-scale-editor {
      .o-threshold {
        .o-select-with-input {
          max-width: 150px;
        }
        .o-threshold-value {
          flex-grow: 1;
          flex-basis: 60%;
          min-width: 0px; /* input overflows in Firefox otherwise */
        }
        .o-threshold-value input:disabled {
          background-color: #edebed;
        }
      }
    }
    .o-cf-iconset-rule {
      .o-cf-clickable-icon {
        border: 1px solid ${GRAY_200};
        border-radius: 4px;
        cursor: pointer;
        &:hover {
          border-color: ${ACTION_COLOR};
          background-color: ${BADGE_SELECTED_COLOR};
        }
        .o-icon {
          width: ${CF_ICON_EDGE_LENGTH}px;
          height: ${CF_ICON_EDGE_LENGTH}px;
        }
      }
      .o-cf-iconsets {
        gap: 11px;
        .o-cf-iconset {
          padding: 7px 8px;
          width: 95px;
          .o-icon {
            margin: 0 3px;
          }
          svg {
            vertical-align: baseline;
          }
        }
      }
      .o-inflection {
        .o-cf-icon-button {
          padding: 4px 10px;
        }
        table {
          font-size: 13px;
          td {
            padding: 6px 0;
          }

          th.o-cf-iconset-icons {
            width: 25px;
          }
          th.o-cf-iconset-text {
            width: 82px;
          }
          th.o-cf-iconset-operator {
            width: 20px;
          }
          .o-cf-iconset-type {
            min-width: 80px;
          }
        }
      }
    }

    .o-icon.arrow-down {
      color: #e06666;
    }
    .o-icon.arrow-up {
      color: #6aa84f;
    }
  }
`;
class ConditionalFormattingEditor extends Component {
    static template = "o-spreadsheet-ConditionalFormattingEditor";
    static props = {
        editedCf: Object,
        onCancel: Function,
        onExit: Function,
        isNewCf: Boolean,
    };
    static components = {
        SelectionInput,
        IconPicker,
        ColorPickerWidget,
        ConditionalFormatPreviewList,
        Section,
        RoundColorPicker,
        StandaloneComposer,
        BadgeSelection,
        ValidationMessages,
        SelectMenu,
    };
    icons = ICONS;
    iconSets = ICON_SETS;
    getTextDecoration = getTextDecoration;
    colorNumberToHex = colorNumberToHex;
    state;
    setup() {
        this.state = useState({
            errors: [],
            currentCFType: this.props.editedCf.rule.type,
            ranges: this.props.editedCf.ranges,
            rules: this.getDefaultRules(),
            hasEditedCf: this.props.isNewCf,
        });
        switch (this.props.editedCf.rule.type) {
            case "CellIsRule":
                this.state.rules.cellIs = this.props.editedCf.rule;
                break;
            case "ColorScaleRule":
                this.state.rules.colorScale = this.props.editedCf.rule;
                break;
            case "IconSetRule":
                this.state.rules.iconSet = this.props.editedCf.rule;
                break;
            case "DataBarRule":
                this.state.rules.dataBar = this.props.editedCf.rule;
                break;
        }
        useExternalListener(window, "click", this.closeMenus);
    }
    get isRangeValid() {
        return this.state.errors.includes("EmptyRange" /* CommandResult.EmptyRange */);
    }
    get errorMessages() {
        return this.state.errors.map((error) => CfTerms.Errors[error] || CfTerms.Errors.Unexpected);
    }
    get cfTypesValues() {
        return [
            { value: "CellIsRule", label: _t("Single color") },
            { value: "ColorScaleRule", label: _t("Color scale") },
            { value: "IconSetRule", label: _t("Icon set") },
            { value: "DataBarRule", label: _t("Data bar") },
        ];
    }
    updateConditionalFormat(newCf) {
        const ranges = newCf.ranges || this.state.ranges;
        const invalidRanges = this.state.ranges.some((xc) => !xc.match(rangeReference));
        if (invalidRanges) {
            if (!newCf.suppressErrors) {
                this.state.errors = ["InvalidRange" /* CommandResult.InvalidRange */];
            }
            return ["InvalidRange" /* CommandResult.InvalidRange */];
        }
        const sheetId = this.env.model.getters.getActiveSheetId();
        const locale = this.env.model.getters.getLocale();
        const rule = newCf.rule || this.getEditedRule(this.state.currentCFType);
        const result = this.env.model.dispatch("ADD_CONDITIONAL_FORMAT", {
            cf: {
                rule: canonicalizeCFRule(rule, locale),
                id: this.props.editedCf.id,
            },
            ranges: ranges.map((xc) => this.env.model.getters.getRangeDataFromXc(sheetId, xc)),
            sheetId,
        });
        if (result.isSuccessful) {
            this.state.hasEditedCf = true;
        }
        const reasons = result.reasons.filter((r) => r !== "NoChanges" /* CommandResult.NoChanges */);
        if (!newCf.suppressErrors) {
            this.state.errors = reasons;
        }
        return reasons;
    }
    getEditedRule(ruleType) {
        switch (ruleType) {
            case "CellIsRule":
                return this.state.rules.cellIs;
            case "ColorScaleRule":
                return this.state.rules.colorScale;
            case "IconSetRule":
                return this.state.rules.iconSet;
            case "DataBarRule":
                return this.state.rules.dataBar;
        }
    }
    onSave() {
        const result = this.updateConditionalFormat({});
        if (result.length === 0) {
            this.props.onExit();
        }
    }
    onCancel() {
        if (this.state.hasEditedCf) {
            this.props.onCancel();
        }
        else {
            this.props.onExit();
        }
    }
    getDefaultRules() {
        return {
            cellIs: {
                type: "CellIsRule",
                operator: "isNotEmpty",
                values: [],
                style: { fillColor: "#b6d7a8" },
            },
            colorScale: {
                type: "ColorScaleRule",
                minimum: { type: "value", color: hexaToInt("EFF7FF") },
                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",
                },
            },
            dataBar: {
                type: "DataBarRule",
                color: 0xd9ead3,
            },
        };
    }
    changeRuleType(ruleType) {
        if (this.state.currentCFType === ruleType) {
            return;
        }
        this.state.errors = [];
        this.state.currentCFType = ruleType;
        this.updateConditionalFormat({ rule: this.getEditedRule(ruleType), suppressErrors: true });
    }
    onRangeUpdate(ranges) {
        this.state.ranges = ranges;
    }
    onRangeConfirmed() {
        this.updateConditionalFormat({ ranges: this.state.ranges });
    }
    /*****************************************************************************
     * Common
     ****************************************************************************/
    toggleMenu(menu) {
        const isSelected = this.state.openedMenu === menu;
        this.closeMenus();
        if (!isSelected) {
            this.state.openedMenu = menu;
        }
    }
    closeMenus() {
        this.state.openedMenu = undefined;
    }
    /*****************************************************************************
     * Cell Is Rule
     ****************************************************************************/
    get isValue1Invalid() {
        return (this.state.errors.includes("FirstArgMissing" /* CommandResult.FirstArgMissing */) ||
            this.state.errors.includes("ValueCellIsInvalidFormula" /* CommandResult.ValueCellIsInvalidFormula */));
    }
    get isValue2Invalid() {
        return this.state.errors.includes("SecondArgMissing" /* CommandResult.SecondArgMissing */);
    }
    toggleStyle(tool) {
        const style = this.state.rules.cellIs.style;
        style[tool] = !style[tool];
        this.updateConditionalFormat({ rule: this.state.rules.cellIs });
        this.closeMenus();
    }
    onKeydown(event) {
        if (event.key === "F4") {
            const target = event.target;
            const update = cycleFixedReference({ start: target.selectionStart ?? 0, end: target.selectionEnd ?? 0 }, target.value, this.env.model.getters.getLocale());
            if (!update) {
                return;
            }
            target.value = update.content;
            target.setSelectionRange(update.selection.start, update.selection.end);
            target.dispatchEvent(new Event("input"));
        }
    }
    setColor(target, color) {
        this.state.rules.cellIs.style[target] = color;
        this.updateConditionalFormat({ rule: this.state.rules.cellIs });
        this.closeMenus();
    }
    editOperator(operator) {
        this.state.rules.cellIs.operator = operator;
        this.updateConditionalFormat({ rule: this.state.rules.cellIs, suppressErrors: true });
        this.closeMenus();
    }
    get cfCriterionMenuItems() {
        return getCriterionMenuItems((type) => this.editOperator(type), availableConditionalFormatOperators);
    }
    get selectedCriterionName() {
        return criterionEvaluatorRegistry.get(this.state.rules.cellIs.operator).name;
    }
    get criterionComponent() {
        return criterionComponentRegistry.get(this.state.rules.cellIs.operator).component;
    }
    get genericCriterion() {
        return {
            type: this.state.rules.cellIs.operator,
            values: this.state.rules.cellIs.values,
        };
    }
    onRuleValuesChanged(rule) {
        this.state.rules.cellIs.values = rule.values;
        this.updateConditionalFormat({ rule: { ...this.state.rules.cellIs, values: rule.values } });
    }
    /*****************************************************************************
     * Color Scale Rule
     ****************************************************************************/
    isValueInvalid(threshold) {
        switch (threshold) {
            case "minimum":
                return (this.state.errors.includes("MinInvalidFormula" /* CommandResult.MinInvalidFormula */) ||
                    this.state.errors.includes("MinBiggerThanMid" /* CommandResult.MinBiggerThanMid */) ||
                    this.state.errors.includes("MinBiggerThanMax" /* CommandResult.MinBiggerThanMax */) ||
                    this.state.errors.includes("MinNaN" /* CommandResult.MinNaN */));
            case "midpoint":
                return (this.state.errors.includes("MidInvalidFormula" /* CommandResult.MidInvalidFormula */) ||
                    this.state.errors.includes("MidNaN" /* CommandResult.MidNaN */) ||
                    this.state.errors.includes("MidBiggerThanMax" /* CommandResult.MidBiggerThanMax */));
            case "maximum":
                return (this.state.errors.includes("MaxInvalidFormula" /* CommandResult.MaxInvalidFormula */) ||
                    this.state.errors.includes("MaxNaN" /* CommandResult.MaxNaN */));
            default:
                return false;
        }
    }
    setColorScaleColor(target, color) {
        if (!isColorValid(color)) {
            return;
        }
        const point = this.state.rules.colorScale[target];
        if (point) {
            point.color = colorToNumber(color);
        }
        this.updateConditionalFormat({ rule: this.state.rules.colorScale });
        this.closeMenus();
    }
    getPreviewGradient() {
        const rule = this.state.rules.colorScale;
        const minColor = colorNumberToHex(rule.minimum.color);
        const midColor = colorNumberToHex(rule.midpoint?.color || DEFAULT_COLOR_SCALE_MIDPOINT_COLOR);
        const maxColor = colorNumberToHex(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
            ? colorNumberToHex(threshold.color)
            : colorNumberToHex(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,
            };
        }
        this.updateConditionalFormat({ rule, suppressErrors: true });
    }
    updateThresholdType(threshold, thresholdType) {
        this.state.rules.colorScale[threshold].type = thresholdType;
        this.updateConditionalFormat({ rule: this.state.rules.colorScale, suppressErrors: true });
    }
    updateThresholdValue(threshold, value) {
        this.state.rules.colorScale[threshold].value = value;
        this.updateConditionalFormat({ rule: this.state.rules.colorScale });
    }
    /*****************************************************************************
     * Icon Set
     ****************************************************************************/
    isInflectionPointInvalid(inflectionPoint) {
        switch (inflectionPoint) {
            case "lowerInflectionPoint":
                return (this.state.errors.includes("ValueLowerInflectionNaN" /* CommandResult.ValueLowerInflectionNaN */) ||
                    this.state.errors.includes("ValueLowerInvalidFormula" /* CommandResult.ValueLowerInvalidFormula */) ||
                    this.state.errors.includes("LowerBiggerThanUpper" /* CommandResult.LowerBiggerThanUpper */));
            case "upperInflectionPoint":
                return (this.state.errors.includes("ValueUpperInflectionNaN" /* CommandResult.ValueUpperInflectionNaN */) ||
                    this.state.errors.includes("ValueUpperInvalidFormula" /* CommandResult.ValueUpperInvalidFormula */) ||
                    this.state.errors.includes("LowerBiggerThanUpper" /* CommandResult.LowerBiggerThanUpper */));
            default:
                return true;
        }
    }
    reverseIcons() {
        const icons = this.state.rules.iconSet.icons;
        const upper = icons.upper;
        icons.upper = icons.lower;
        icons.lower = upper;
        this.updateConditionalFormat({ rule: this.state.rules.iconSet });
    }
    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;
        this.updateConditionalFormat({ rule: this.state.rules.iconSet });
    }
    setIcon(target, icon) {
        this.state.rules.iconSet.icons[target] = icon;
        this.updateConditionalFormat({ rule: this.state.rules.iconSet });
    }
    setInflectionOperator(inflectionPoint, operator) {
        this.state.rules.iconSet[inflectionPoint].operator = operator;
        this.updateConditionalFormat({ rule: this.state.rules.iconSet });
    }
    setInflectionValue(inflectionPoint, value) {
        this.state.rules.iconSet[inflectionPoint].value = value;
        this.updateConditionalFormat({ rule: this.state.rules.iconSet });
    }
    setInflectionType(inflectionPoint, type, ev) {
        this.state.rules.iconSet[inflectionPoint].type = type;
        this.updateConditionalFormat({ rule: this.state.rules.iconSet, suppressErrors: true });
    }
    getColorScaleComposerProps(thresholdType) {
        const threshold = this.state.rules.colorScale[thresholdType];
        if (!threshold) {
            throw new Error("Threshold not found");
        }
        const isInvalid = this.isValueInvalid(thresholdType);
        return {
            onConfirm: (str) => {
                threshold.value = str;
                this.updateConditionalFormat({ rule: this.state.rules.colorScale });
            },
            composerContent: threshold.value || "",
            placeholder: _t("Formula"),
            defaultStatic: true,
            invalid: isInvalid,
            class: "o-sidePanel-composer",
            defaultRangeSheetId: this.env.model.getters.getActiveSheetId(),
        };
    }
    getColorIconSetComposerProps(inflectionPoint) {
        const inflection = this.state.rules.iconSet[inflectionPoint];
        const isInvalid = this.isInflectionPointInvalid(inflectionPoint);
        return {
            onConfirm: (str) => {
                inflection.value = str;
                this.updateConditionalFormat({ rule: this.state.rules.iconSet });
            },
            composerContent: inflection.value || "",
            placeholder: _t("Formula"),
            defaultStatic: true,
            invalid: isInvalid,
            class: "o-sidePanel-composer",
            defaultRangeSheetId: this.env.model.getters.getActiveSheetId(),
        };
    }
    /*****************************************************************************
     * DataBar
     ****************************************************************************/
    getRangeValues() {
        return [this.state.rules.dataBar.rangeValues || ""];
    }
    updateDataBarColor(color) {
        if (!isColorValid(color)) {
            return;
        }
        this.state.rules.dataBar.color = Number.parseInt(color.slice(1), 16);
        this.updateConditionalFormat({ rule: this.state.rules.dataBar });
    }
    onDataBarRangeUpdate(ranges) {
        this.state.rules.dataBar.rangeValues = ranges[0];
    }
    onDataBarRangeChange() {
        this.updateConditionalFormat({ rule: this.state.rules.dataBar });
    }
}

class ConditionalFormattingPanel extends Component {
    static template = "o-spreadsheet-ConditionalFormattingPanel";
    static props = {
        selection: { type: Object, optional: true },
        onCloseSidePanel: Function,
    };
    static components = {
        ConditionalFormatPreviewList,
        ConditionalFormattingEditor,
        Section,
    };
    activeSheetId;
    originalEditedCf = undefined;
    state = useState({
        mode: "list",
    });
    setup() {
        this.activeSheetId = this.env.model.getters.getActiveSheetId();
        const sheetId = this.env.model.getters.getActiveSheetId();
        const rules = this.env.model.getters.getRulesSelection(sheetId, this.props.selection || []);
        if (rules.length === 1) {
            const cf = this.conditionalFormats.find((c) => c.id === rules[0]);
            if (cf) {
                this.editConditionalFormat(cf);
            }
        }
        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();
                }
            }
            else if (!this.editedCF) {
                this.switchToList();
            }
        });
    }
    get conditionalFormats() {
        const cfs = this.env.model.getters.getConditionalFormats(this.env.model.getters.getActiveSheetId());
        return cfs.map((cf) => ({
            ...cf,
            rule: localizeCFRule(cf.rule, this.env.model.getters.getLocale()),
        }));
    }
    switchToList() {
        this.state.mode = "list";
        this.state.editedCfId = undefined;
        this.originalEditedCf = undefined;
    }
    addConditionalFormat() {
        const cfId = this.env.model.uuidGenerator.smallUuid();
        this.env.model.dispatch("ADD_CONDITIONAL_FORMAT", {
            sheetId: this.activeSheetId,
            ranges: this.env.model.getters
                .getSelectedZones()
                .map((zone) => this.env.model.getters.getRangeDataFromZone(this.activeSheetId, zone)),
            cf: {
                id: cfId,
                rule: {
                    type: "CellIsRule",
                    operator: "isNotEmpty",
                    style: { fillColor: "#b6d7a8" },
                    values: [],
                },
            },
        });
        this.state.editedCfId = cfId;
        this.state.mode = "edit";
        this.originalEditedCf = undefined;
    }
    editConditionalFormat(cf) {
        this.state.mode = "edit";
        this.state.editedCfId = cf.id;
        this.originalEditedCf = cf;
    }
    cancelEdition() {
        if (this.originalEditedCf) {
            this.env.model.dispatch("ADD_CONDITIONAL_FORMAT", {
                sheetId: this.activeSheetId,
                ranges: this.originalEditedCf.ranges.map((range) => this.env.model.getters.getRangeDataFromXc(this.activeSheetId, range)),
                cf: this.originalEditedCf,
            });
        }
        else if (this.state.editedCfId) {
            this.env.model.dispatch("REMOVE_CONDITIONAL_FORMAT", {
                sheetId: this.activeSheetId,
                id: this.state.editedCfId,
            });
        }
        this.switchToList();
    }
    get editedCF() {
        return this.conditionalFormats.find((cf) => cf.id === this.state.editedCfId);
    }
}

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

css /* scss */ `
  .o-custom-currency {
    .o-format-proposals {
      color: black;
    }

    .o-format-examples {
      background: #f9fafb;
      padding: 8px;
      border-radius: 4px;
      border: 1px solid #d8dadd;
      color: #374151;
    }

    table {
      table-layout: fixed;
    }
  }
`;
class CustomCurrencyPanel extends Component {
    static template = "o-spreadsheet-CustomCurrencyPanel";
    static components = { Section, Checkbox };
    static props = { onCloseSidePanel: Function };
    availableCurrencies;
    state;
    setup() {
        this.availableCurrencies = [];
        this.state = useState({
            selectedCurrencyIndex: 0,
            currencyCode: "",
            currencySymbol: "",
            selectedFormatIndex: 0,
            isAccountingFormat: false,
        });
        onWillStart(() => this.updateAvailableCurrencies());
    }
    get formatProposals() {
        const baseCurrency = this.availableCurrencies[this.state.selectedCurrencyIndex];
        const position = baseCurrency.position;
        const opposite = baseCurrency.position === "before" ? "after" : "before";
        const symbol = this.state.currencySymbol.trim() ? this.state.currencySymbol : "";
        const code = this.state.currencyCode.trim() ? this.state.currencyCode : "";
        const decimalPlaces = baseCurrency.decimalPlaces;
        if (!symbol && !code) {
            return [];
        }
        const simple = { symbol, position, decimalPlaces };
        const rounded = { symbol, position, decimalPlaces: 0 };
        const simpleWithCode = { symbol, position, decimalPlaces, code };
        const roundedWithCode = { symbol, position, decimalPlaces: 0, code };
        const simpleOpposite = { symbol, position: opposite, decimalPlaces };
        const roundedOpposite = { symbol, position: opposite, decimalPlaces: 0 };
        const simpleOppositeWithCode = { symbol, position: opposite, decimalPlaces, code };
        const roundedOppositeWithCode = { symbol, position: opposite, decimalPlaces: 0, code };
        const currencies = [
            rounded,
            simple,
            roundedWithCode,
            simpleWithCode,
            roundedOpposite,
            simpleOpposite,
            roundedOppositeWithCode,
            simpleOppositeWithCode,
        ];
        const usedFormats = new Set();
        const locale = this.env.model.getters.getLocale();
        return currencies
            .map((currency) => {
            const format = createCurrencyFormat(currency);
            if ((!currency.symbol && !currency.code) || usedFormats.has(format)) {
                return undefined;
            }
            usedFormats.add(format);
            return {
                format,
                accountingFormat: createAccountingFormat(currency),
                example: formatValue(1000.0, { format, locale }),
            };
        })
            .filter(isDefined);
    }
    get isSameFormat() {
        return this.selectedFormat ? this.selectedFormat === this.getCommonFormat() : false;
    }
    async updateAvailableCurrencies() {
        if (currenciesRegistry.getAll().length === 0) {
            const currencies = (await this.env.loadCurrencies?.()) || [];
            currencies.forEach((currency, index) => {
                currenciesRegistry.add(index.toString(), currency);
            });
        }
        const emptyCurrency = {
            name: 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() {
        this.env.model.dispatch("SET_FORMATTING_WITH_PIVOT", {
            sheetId: this.env.model.getters.getActiveSheetId(),
            target: this.env.model.getters.getSelectedZones(),
            format: this.selectedFormat,
        });
    }
    // ---------------------------------------------------------------------------
    // Private
    // ---------------------------------------------------------------------------
    initAvailableCurrencies() {
        this.state.selectedCurrencyIndex = 0;
    }
    getCommonFormat() {
        const selectedZones = this.env.model.getters.getSelectedZones();
        const sheetId = this.env.model.getters.getActiveSheetId();
        const cells = selectedZones
            .map((zone) => this.env.model.getters.getEvaluatedCellsInZone(sheetId, zone))
            .flat();
        const firstFormat = cells[0].format;
        return cells.every((cell) => cell.format === firstFormat) ? firstFormat : undefined;
    }
    currencyDisplayName(currency) {
        return currency.name + (currency.code ? ` (${currency.code})` : "");
    }
    toggleAccountingFormat() {
        this.state.isAccountingFormat = !this.state.isAccountingFormat;
    }
    getFormatExamples() {
        const format = this.selectedFormat;
        const locale = this.env.model.getters.getLocale();
        return [
            { label: _t("positive") + ":", value: formatValue(1234.56, { format, locale }) },
            { label: _t("negative") + ":", value: formatValue(-1234.56, { format, locale }) },
            { label: _t("zero") + ":", value: formatValue(0, { format, locale }) },
        ];
    }
    get selectedFormat() {
        const proposal = this.formatProposals[this.state.selectedFormatIndex];
        return this.state.isAccountingFormat ? proposal?.accountingFormat : proposal?.format;
    }
}

class DataValidationEditor extends Component {
    static template = "o-spreadsheet-DataValidationEditor";
    static components = { SelectionInput, SelectMenu, Section, ValidationMessages };
    static props = {
        rule: { type: Object, optional: true },
        onExit: Function,
        onCloseSidePanel: { type: Function, optional: true },
    };
    state = useState({ rule: this.defaultDataValidationRule, errors: [] });
    editingSheetId;
    setup() {
        this.editingSheetId = this.env.model.getters.getActiveSheetId();
        if (this.props.rule) {
            this.state.rule = {
                ...this.props.rule,
                ranges: this.props.rule.ranges.map((range) => this.env.model.getters.getRangeString(range, this.editingSheetId)),
            };
            this.state.rule.criterion.type = this.props.rule.criterion.type;
        }
    }
    onCriterionTypeChanged(type) {
        this.state.rule.criterion.type = type;
    }
    onRangesChanged(ranges) {
        this.state.rule.ranges = ranges;
    }
    onCriterionChanged(criterion) {
        this.state.rule.criterion = criterion;
    }
    changeRuleIsBlocking(ev) {
        const isBlocking = ev.target.value;
        this.state.rule.isBlocking = isBlocking === "true";
    }
    onSave() {
        if (this.state.rule) {
            const result = this.env.model.dispatch("ADD_DATA_VALIDATION_RULE", this.dispatchPayload);
            if (!result.isSuccessful) {
                this.state.errors = result.reasons;
            }
            else {
                this.props.onExit();
            }
        }
    }
    get dispatchPayload() {
        const rule = { ...this.state.rule, ranges: undefined };
        const locale = this.env.model.getters.getLocale();
        const criterion = rule.criterion;
        const criterionEvaluator = criterionEvaluatorRegistry.get(criterion.type);
        const values = criterion.values
            .slice(0, criterionEvaluator.numberOfValues(criterion))
            .map((value) => value?.trim())
            .filter((value) => value !== "" && value !== undefined)
            .map((value) => canonicalizeContent(value, locale));
        rule.criterion = { ...criterion, values };
        return {
            sheetId: this.editingSheetId,
            ranges: this.state.rule.ranges.map((xc) => this.env.model.getters.getRangeDataFromXc(this.editingSheetId, xc)),
            rule,
        };
    }
    get dvCriterionMenuItems() {
        return getCriterionMenuItems((type) => this.onCriterionTypeChanged(type), availableDataValidationOperators);
    }
    get selectedCriterionName() {
        const selectedType = this.state.rule.criterion.type;
        return criterionEvaluatorRegistry.get(selectedType).name;
    }
    get defaultDataValidationRule() {
        const sheetId = this.env.model.getters.getActiveSheetId();
        const ranges = this.env.model.getters
            .getSelectedZones()
            .map((zone) => zoneToXc(this.env.model.getters.getUnboundedZone(sheetId, zone)));
        return {
            id: this.env.model.uuidGenerator.smallUuid(),
            criterion: { type: "containsText", values: [""] },
            ranges,
        };
    }
    get criterionComponent() {
        return criterionComponentRegistry.get(this.state.rule.criterion.type).component;
    }
    get errorMessages() {
        return this.state.errors.map((error) => DVTerms.Errors[error] || DVTerms.Errors.Unexpected);
    }
}

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

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

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

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

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

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

const FIND_AND_REPLACE_HIGHLIGHT_COLOR = "#8B008B";
var Direction;
(function (Direction) {
    Direction[Direction["previous"] = -1] = "previous";
    Direction[Direction["current"] = 0] = "current";
    Direction[Direction["next"] = 1] = "next";
})(Direction || (Direction = {}));
class FindAndReplaceStore extends SpreadsheetStore {
    mutators = [
        "updateSearchOptions",
        "updateSearchContent",
        "searchFormulas",
        "selectPreviousMatch",
        "selectNextMatch",
        "replace",
    ];
    allSheetsMatches = [];
    activeSheetMatches = [];
    specificRangeMatches = [];
    currentSearchRegex = null;
    initialShowFormulaState;
    preserveSelectedMatchIndex = false;
    irreplaceableMatchCount = 0;
    isSearchDirty = false;
    shouldFinalizeUpdateSelection = false;
    notificationStore = this.get(NotificationStore);
    // fixme: why do we make selectedMatchIndex on top of a selected
    // property in the matches?
    selectedMatchIndex = null;
    toSearch = "";
    toReplace = "";
    searchOptions = {
        matchCase: false,
        exactMatch: false,
        searchFormulas: false,
        searchScope: "activeSheet",
        specificRange: undefined,
    };
    constructor(get) {
        super(get);
        this.initialShowFormulaState = this.model.getters.shouldShowFormulas();
        this.searchOptions.searchFormulas = this.initialShowFormulaState;
        const highlightStore = get(HighlightStore);
        highlightStore.register(this);
        this.onDispose(() => {
            this.model.dispatch("SET_FORMULA_VISIBILITY", { show: this.initialShowFormulaState });
            highlightStore.unRegister(this);
        });
    }
    get searchMatches() {
        switch (this.searchOptions.searchScope) {
            case "allSheets":
                return this.allSheetsMatches;
            case "activeSheet":
                return this.activeSheetMatches;
            case "specificRange":
                return this.specificRangeMatches;
        }
    }
    updateSearchContent(toSearch) {
        this._updateSearch(toSearch, this.searchOptions);
    }
    updateSearchOptions(searchOptions) {
        this._updateSearch(this.toSearch, { ...this.searchOptions, ...searchOptions });
    }
    searchFormulas(showFormula) {
        this.model.dispatch("SET_FORMULA_VISIBILITY", { show: showFormula });
        this.updateSearchOptions({ searchFormulas: showFormula });
    }
    selectPreviousMatch() {
        this.selectNextCell(Direction.previous, {
            jumpToMatchSheet: true,
            updateSelection: true,
        });
    }
    selectNextMatch() {
        this.selectNextCell(Direction.next, { jumpToMatchSheet: true, updateSelection: true });
    }
    handle(cmd) {
        switch (cmd.type) {
            case "SET_FORMULA_VISIBILITY":
                this.updateSearchOptions({ searchFormulas: cmd.show });
                break;
            case "UNDO":
            case "REDO":
            case "REMOVE_TABLE":
            case "UPDATE_FILTER":
            case "REMOVE_COLUMNS_ROWS":
            case "HIDE_COLUMNS_ROWS":
            case "UNHIDE_COLUMNS_ROWS":
            case "ADD_COLUMNS_ROWS":
            case "EVALUATE_CELLS":
            case "UPDATE_CELL":
                this.isSearchDirty = true;
                break;
            case "ACTIVATE_SHEET":
                this.isSearchDirty = true;
                this.shouldFinalizeUpdateSelection = true;
                if (this.searchOptions.specificRange) {
                    this.searchOptions.specificRange = {
                        ...this.searchOptions.specificRange,
                        sheetId: this.getters.getActiveSheetId(),
                    };
                }
                break;
            case "DELETE_SHEET":
                if (this.searchOptions.searchScope === "specificRange" &&
                    this.searchOptions.specificRange?.sheetId === cmd.sheetId) {
                    this.searchOptions = { ...this.searchOptions, specificRange: undefined };
                }
                this.isSearchDirty = true;
                break;
            case "REPLACE_SEARCH":
                for (const match of cmd.matches) {
                    this.replaceMatch(match, cmd.searchString, cmd.replaceWith, cmd.searchOptions);
                }
                if (this.irreplaceableMatchCount > 0) {
                    this.showReplaceWarningMessage(cmd.matches.length, this.irreplaceableMatchCount);
                }
                this.irreplaceableMatchCount = 0;
                break;
        }
    }
    finalize() {
        if (this.isSearchDirty) {
            this.refreshSearch({
                jumpToMatchSheet: false,
                updateSelection: this.shouldFinalizeUpdateSelection,
            });
            this.shouldFinalizeUpdateSelection = false;
            this.isSearchDirty = false;
        }
    }
    get allSheetMatchesCount() {
        return this.allSheetsMatches.length;
    }
    get activeSheetMatchesCount() {
        return this.activeSheetMatches.length;
    }
    get specificRangeMatchesCount() {
        return this.specificRangeMatches.length;
    }
    // ---------------------------------------------------------------------------
    // 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.currentSearchRegex = getSearchRegex(this.toSearch, this.searchOptions);
        this.refreshSearch({ jumpToMatchSheet: true, updateSelection: true });
    }
    /**
     * refresh the matches according to the current search options
     */
    refreshSearch(options) {
        if (!this.preserveSelectedMatchIndex) {
            this.selectedMatchIndex = null;
        }
        this.findMatches();
        this.selectNextCell(Direction.current, options);
    }
    getSheetsInSearchOrder() {
        switch (this.searchOptions.searchScope) {
            case "allSheets":
                const sheetIds = this.getters.getSheetIds();
                const activeSheetIndex = sheetIds.findIndex((id) => id === this.getters.getActiveSheetId());
                return [
                    sheetIds[activeSheetIndex],
                    ...sheetIds.slice(activeSheetIndex + 1),
                    ...sheetIds.slice(0, activeSheetIndex),
                ];
            case "activeSheet":
                return [this.getters.getActiveSheetId()];
            case "specificRange":
                const specificRange = this.searchOptions.specificRange;
                if (!specificRange) {
                    return [];
                }
                return specificRange ? [specificRange.sheetId] : [];
        }
    }
    /**
     * Find matches using the current regex
     */
    findMatches() {
        const matches = [];
        if (this.toSearch) {
            for (const sheetId of this.getters.getSheetIds()) {
                matches.push(...this.findMatchesInSheet(sheetId));
            }
        }
        // set results
        this.allSheetsMatches = matches;
        this.activeSheetMatches = matches.filter((match) => match.sheetId === this.getters.getActiveSheetId());
        if (this.searchOptions.specificRange) {
            const { sheetId, zone } = this.searchOptions.specificRange;
            this.specificRangeMatches = matches.filter((match) => match.sheetId === sheetId && isInside(match.col, match.row, zone));
        }
        else {
            this.specificRangeMatches = [];
        }
    }
    findMatchesInSheet(sheetId) {
        const matches = [];
        const { left, right, top, bottom } = this.getters.getSheetZone(sheetId);
        for (let row = top; row <= bottom; row++) {
            for (let col = left; col <= right; col++) {
                const isColHidden = this.getters.isColHidden(sheetId, col);
                const isRowHidden = this.getters.isRowHidden(sheetId, row);
                if (isColHidden || isRowHidden) {
                    continue;
                }
                const cellPosition = { sheetId, col, row };
                if (this.currentSearchRegex?.test(this.getSearchableString(cellPosition))) {
                    const match = { sheetId, col, row };
                    matches.push(match);
                }
            }
        }
        return matches;
    }
    /**
     * 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, options) {
        const matches = this.searchMatches;
        if (!matches.length) {
            this.selectedMatchIndex = null;
            return;
        }
        let nextIndex;
        if (this.selectedMatchIndex === null) {
            let nextMatchIndex = -1;
            // if search is not available in current sheet will select in next sheet
            for (const sheetId of this.getSheetsInSearchOrder()) {
                nextMatchIndex = matches.findIndex((match) => match.sheetId === sheetId);
                if (nextMatchIndex !== -1) {
                    break;
                }
            }
            nextIndex = nextMatchIndex;
        }
        else {
            nextIndex = this.selectedMatchIndex + indexChange;
        }
        // loop index value inside the array (index -1 => last index)
        nextIndex = (nextIndex + matches.length) % matches.length;
        this.selectedMatchIndex = nextIndex;
        const selectedMatch = matches[nextIndex];
        // Switch to the sheet where the match is located
        if (options.jumpToMatchSheet && this.getters.getActiveSheetId() !== selectedMatch.sheetId) {
            // We set `preserveSelectedMatchIndex` to true to avoid resetting the selected search
            // index in the `refreshSearch` function when a new sheet is activated. The reason being
            // that, when we automatically go back to previous sheet while performing a search, the
            // search index is reset to the first occurrence each time.
            this.preserveSelectedMatchIndex = true;
            this.model.dispatch("ACTIVATE_SHEET", {
                sheetIdFrom: this.getters.getActiveSheetId(),
                sheetIdTo: selectedMatch.sheetId,
            });
            this.preserveSelectedMatchIndex = false;
            // We do not want to reset the selection at finalize in this case
            this.isSearchDirty = false;
        }
        // we want grid selection to capture the selection stream
        this.model.selection.getBackToDefault();
        if (options.updateSelection) {
            this.model.selection.selectCell(selectedMatch.col, selectedMatch.row);
        }
    }
    /**
     * Replace the value of the currently selected match
     */
    replace() {
        if (this.selectedMatchIndex === null) {
            return;
        }
        this.preserveSelectedMatchIndex = true;
        this.shouldFinalizeUpdateSelection = true;
        this.model.dispatch("REPLACE_SEARCH", {
            searchString: this.toSearch,
            replaceWith: this.toReplace,
            matches: [this.searchMatches[this.selectedMatchIndex]],
            searchOptions: this.searchOptions,
        });
        this.preserveSelectedMatchIndex = false;
    }
    /**
     * Apply the replace function to all the matches one time.
     */
    replaceAll() {
        this.model.dispatch("REPLACE_SEARCH", {
            searchString: this.toSearch,
            replaceWith: this.toReplace,
            matches: this.searchMatches,
            searchOptions: this.searchOptions,
        });
    }
    /**
     * Show a warning message based on the number of matches replaced and irreplaceable.
     */
    showReplaceWarningMessage(totalMatches, irreplaceableMatches) {
        const replaceableMatches = totalMatches - irreplaceableMatches;
        if (replaceableMatches === 0) {
            this.notificationStore.notifyUser({
                type: "warning",
                sticky: false,
                text: _t("Match(es) cannot be replaced as they are part of a formula."),
            });
        }
        else {
            this.notificationStore.notifyUser({
                type: "warning",
                sticky: false,
                text: _t("%(replaceable_count)s match(es) replaced. %(irreplaceable_count)s match(es) cannot be replaced as they are part of a formula.", {
                    replaceable_count: replaceableMatches,
                    irreplaceable_count: irreplaceableMatches,
                }),
            });
        }
    }
    replaceMatch(selectedMatch, searchString, replaceWith, searchOptions) {
        const cell = this.getters.getCell(selectedMatch);
        if (!cell?.content) {
            return;
        }
        if (cell?.isFormula && !searchOptions.searchFormulas) {
            this.irreplaceableMatchCount++;
            return;
        }
        const searchRegex = getSearchRegex(searchString, searchOptions);
        const replaceRegex = new RegExp(searchRegex.source, searchRegex.flags + "g");
        const toReplace = this.getters.getCellText(selectedMatch, {
            showFormula: searchOptions.searchFormulas,
        });
        const content = toReplace.replace(replaceRegex, replaceWith);
        const canonicalContent = canonicalizeNumberContent(content, this.getters.getLocale());
        this.model.dispatch("UPDATE_CELL", { ...selectedMatch, content: canonicalContent });
    }
    getSearchableString(position) {
        return this.getters.getCellText(position, { showFormula: this.searchOptions.searchFormulas });
    }
    // ---------------------------------------------------------------------------
    // Grid rendering
    // ---------------------------------------------------------------------------
    get highlights() {
        const highlights = [];
        const sheetId = this.getters.getActiveSheetId();
        for (const [index, match] of this.searchMatches.entries()) {
            if (match.sheetId !== sheetId) {
                continue; // Skip drawing matches from other sheets
            }
            const zone = positionToZone(match);
            const zoneWithMerge = this.getters.expandZone(sheetId, zone);
            const { width, height } = this.getters.getVisibleRect(zoneWithMerge);
            if (width > 0 && height > 0) {
                highlights.push({
                    range: this.model.getters.getRangeFromZone(sheetId, zoneWithMerge),
                    color: FIND_AND_REPLACE_HIGHLIGHT_COLOR,
                    noBorder: index !== this.selectedMatchIndex,
                    thinLine: true,
                    fillAlpha: 0.2,
                });
            }
        }
        if (this.searchOptions.searchScope === "specificRange") {
            const range = this.searchOptions.specificRange;
            if (range && range.sheetId === sheetId) {
                highlights.push({
                    range,
                    color: FIND_AND_REPLACE_HIGHLIGHT_COLOR,
                    noFill: true,
                    thinLine: true,
                });
            }
        }
        return highlights;
    }
}

css /* scss */ `
  .o-find-and-replace {
    outline: none;
    height: 100%;
    .o-input-search-container {
      display: flex;
      .o-input-with-count {
        flex-grow: 1;
        width: auto;
      }
      .o-input-without-count {
        width: 100%;
      }
      .o-input-count {
        width: fit-content;
        padding: 4px 0 4px 4px;
        white-space: nowrap;
      }
    }

    .o-result-buttons {
      .o-button {
        height: 19px;
        width: 19px;
        box-sizing: content-box;
        .o-icon {
          height: 14px;
          width: 14px;
        }
      }
    }
  }
`;
class FindAndReplacePanel extends Component {
    static template = "o-spreadsheet-FindAndReplacePanel";
    static components = { SelectionInput, Section, Checkbox, ValidationMessages };
    static props = {
        onCloseSidePanel: Function,
    };
    searchInput = useRef("searchInput");
    store;
    state;
    updateSearchContent;
    get hasSearchResult() {
        return this.store.selectedMatchIndex !== null;
    }
    get searchOptions() {
        return this.store.searchOptions;
    }
    get allSheetsMatchesCount() {
        return _t("%s matches in all sheets", this.store.allSheetMatchesCount);
    }
    get currentSheetMatchesCount() {
        return _t("%(matches)s matches in %(sheetName)s", {
            matches: this.store.activeSheetMatchesCount,
            sheetName: this.env.model.getters.getSheetName(this.env.model.getters.getActiveSheetId()),
        });
    }
    get specificRangeMatchesCount() {
        const range = this.searchOptions.specificRange;
        if (!range) {
            return "";
        }
        const { sheetId, zone } = range;
        return _t("%(matches)s matches in range %(range)s of %(sheetName)s", {
            matches: this.store.specificRangeMatchesCount,
            range: zoneToXc(zone),
            sheetName: this.env.model.getters.getSheetName(sheetId),
        });
    }
    get searchInfo() {
        if (!this.store.toSearch) {
            return [];
        }
        return [
            this.specificRangeMatchesCount,
            this.currentSheetMatchesCount,
            this.allSheetsMatchesCount,
        ];
    }
    setup() {
        this.store = useLocalStore(FindAndReplaceStore);
        this.state = useState({ dataRange: "" });
        onMounted(() => this.searchInput.el?.focus());
        onWillUnmount(() => this.updateSearchContent.stopDebounce());
        this.updateSearchContent = debounce(this.store.updateSearchContent, 200);
        useExternalListener(window, "keydown", (ev) => {
            const code = keyboardEventToShortcutString(ev);
            if (code === "Ctrl+F" || code === "Ctrl+H") {
                this.searchInput.el?.focus();
                ev.preventDefault();
                ev.stopPropagation();
            }
        }, { capture: true });
    }
    onFocusSearch() {
        this.updateDataRange();
    }
    onSearchInput(ev) {
        this.updateSearchContent(ev.target.value);
    }
    onKeydownSearch(ev) {
        if (ev.key === "Enter") {
            ev.preventDefault();
            ev.stopPropagation();
            ev.shiftKey ? this.store.selectPreviousMatch() : this.store.selectNextMatch();
        }
    }
    onKeydownReplace(ev) {
        if (ev.key === "Enter") {
            ev.preventDefault();
            ev.stopPropagation();
            this.store.replace();
        }
    }
    searchFormulas(searchFormulas) {
        this.store.searchFormulas(searchFormulas);
    }
    searchExactMatch(exactMatch) {
        this.store.updateSearchOptions({ exactMatch });
    }
    searchMatchCase(matchCase) {
        this.store.updateSearchOptions({ matchCase });
    }
    changeSearchScope(ev) {
        const searchScope = ev.target.value;
        this.store.updateSearchOptions({ searchScope });
    }
    onSearchRangeChanged(ranges) {
        this.state.dataRange = ranges[0];
    }
    updateDataRange() {
        if (!this.state.dataRange || this.searchOptions.searchScope !== "specificRange") {
            return;
        }
        const specificRange = this.env.model.getters.getRangeFromSheetXC(this.env.model.getters.getActiveSheetId(), this.state.dataRange);
        this.store.updateSearchOptions({ specificRange });
    }
    get specificRange() {
        const range = this.store.searchOptions.specificRange;
        return range ? this.env.model.getters.getRangeString(range, "forceSheetReference") : "";
    }
    get pendingSearch() {
        return this.updateSearchContent.isDebouncePending();
    }
    get selectionInputKey() {
        // Selections input are made to work with objects linked to a sheet id. They store the active sheet id at their creation,
        // and have specific behaviour linked to it (eg. go back to the initial sheet after confirmation).
        // We don't want all those behaviors here, so we force the recreation of the component when the active sheet changes.
        // The only drawback is that the input loses focus when changing sheet.
        return this.env.model.getters.getActiveSheetId();
    }
}

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

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

class PivotMeasureDisplayPanelStore extends SpreadsheetStore {
    pivotId;
    initialMeasure;
    mutators = [
        "cancelMeasureDisplayEdition",
        "updateMeasureDisplayType",
        "updateMeasureDisplayField",
        "updateMeasureDisplayValue",
    ];
    measureDisplay;
    constructor(get, pivotId, initialMeasure) {
        super(get);
        this.pivotId = pivotId;
        this.initialMeasure = initialMeasure;
        this.measureDisplay = initialMeasure.display || { type: "no_calculations" };
    }
    updateMeasureDisplayType(measureDisplayType) {
        this.updatePivotMeasureDisplay(this.getMeasureDisplay(measureDisplayType, this.measureDisplay.fieldNameWithGranularity, this.measureDisplay.value));
    }
    updateMeasureDisplayField(fieldNameWithGranularity) {
        this.updatePivotMeasureDisplay(this.getMeasureDisplay(this.measureDisplay.type, fieldNameWithGranularity, this.measureDisplay.value));
    }
    updateMeasureDisplayValue(value) {
        this.updatePivotMeasureDisplay(this.getMeasureDisplay(this.measureDisplay.type, this.measureDisplay.fieldNameWithGranularity, value));
    }
    updatePivotMeasureDisplay(newDisplay) {
        const pivotDefinition = deepCopy(this.model.getters.getPivotCoreDefinition(this.pivotId));
        const measureIndex = this.getMeasureIndex(this.initialMeasure.id, pivotDefinition);
        const newMeasure = { ...pivotDefinition.measures[measureIndex], display: newDisplay };
        pivotDefinition.measures[measureIndex] = newMeasure;
        const result = this.model.dispatch("UPDATE_PIVOT", {
            pivot: pivotDefinition,
            pivotId: this.pivotId,
        });
        if (result.isSuccessful) {
            this.measureDisplay = newDisplay;
        }
    }
    getMeasureDisplay(measureDisplayType, fieldNameWithGranularity, value) {
        switch (measureDisplayType) {
            case "no_calculations":
            case "%_of_grand_total":
            case "%_of_col_total":
            case "%_of_row_total":
            case "%_of_parent_row_total":
            case "%_of_parent_col_total":
            case "index":
                return { type: measureDisplayType };
            case "%_of_parent_total":
            case "running_total":
            case "%_running_total":
            case "rank_asc":
            case "rank_desc":
                if (!fieldNameWithGranularity) {
                    fieldNameWithGranularity = this.fields[0]?.nameWithGranularity;
                }
                return { type: measureDisplayType, fieldNameWithGranularity };
            case "%_of":
            case "difference_from":
            case "%_difference_from":
                if (!fieldNameWithGranularity) {
                    fieldNameWithGranularity = this.fields[0]?.nameWithGranularity;
                }
                const possibleValues = this.getPossibleValues(fieldNameWithGranularity);
                if (value === undefined || !possibleValues.find((v) => v.value === value)) {
                    value = PREVIOUS_VALUE;
                }
                return {
                    type: measureDisplayType,
                    fieldNameWithGranularity,
                    value: value ?? PREVIOUS_VALUE,
                };
        }
    }
    getMeasureIndex(measureId, pivotDefinition) {
        const measureIndex = pivotDefinition.measures.findIndex((m) => m.id === measureId);
        if (measureIndex === -1) {
            throw new Error(`Measure with id ${measureId} not found in pivot.`);
        }
        return measureIndex;
    }
    get doesDisplayNeedsField() {
        return (["%_of_parent_total", "running_total", "%_running_total", "rank_asc", "rank_desc"].includes(this.measureDisplay.type) || this.doesDisplayNeedsValue);
    }
    get fields() {
        const definition = this.getters.getPivot(this.pivotId).definition;
        return [...definition.columns, ...definition.rows].map((f) => ({
            ...f,
            displayName: getFieldDisplayName(f),
        }));
    }
    get doesDisplayNeedsValue() {
        return this.isDisplayValueDependant(this.measureDisplay);
    }
    isDisplayValueDependant(display) {
        return ["%_of", "difference_from", "%_difference_from"].includes(display.type);
    }
    get values() {
        const display = this.measureDisplay;
        if (!this.isDisplayValueDependant(display)) {
            return [];
        }
        return this.getPossibleValues(display.fieldNameWithGranularity);
    }
    getPossibleValues(fieldNameWithGranularity) {
        const baseValues = [
            { value: PREVIOUS_VALUE, label: _t("(previous)") },
            { value: NEXT_VALUE, label: _t("(next)") },
        ];
        const field = this.fields.find((f) => f.nameWithGranularity === fieldNameWithGranularity);
        if (!field) {
            return [];
        }
        const values = this.getters.getPivot(this.pivotId).getPossibleFieldValues(field);
        return [...baseValues, ...values];
    }
    cancelMeasureDisplayEdition() {
        const pivotDefinition = deepCopy(this.model.getters.getPivotCoreDefinition(this.pivotId));
        const measureIndex = this.getMeasureIndex(this.initialMeasure.id, pivotDefinition);
        pivotDefinition.measures[measureIndex] = {
            ...pivotDefinition.measures[measureIndex],
            display: this.initialMeasure.display,
        };
        this.model.dispatch("UPDATE_PIVOT", {
            pivot: pivotDefinition,
            pivotId: this.pivotId,
        });
    }
}

css /* scss */ `
  .o-sidePanel {
    .o-pivot-measure-display-field,
    .o-pivot-measure-display-value {
      border: solid 1px ${GRAY_300};
      border-radius: 3px;
    }

    .o-pivot-measure-display-description {
      white-space: pre-wrap;
      color: dimgray;
      border-left: 2px solid ${GRAY_300};
    }
  }
`;
class PivotMeasureDisplayPanel extends Component {
    static template = "o-spreadsheet-PivotMeasureDisplayPanel";
    static props = {
        onCloseSidePanel: Function,
        pivotId: String,
        measure: Object,
    };
    static components = { Section, Checkbox, RadioSelection };
    measureDisplayTypeLabels = measureDisplayTerms.labels;
    measureDisplayDescription = measureDisplayTerms.documentation;
    store;
    setup() {
        this.store = useLocalStore(PivotMeasureDisplayPanelStore, this.props.pivotId, this.props.measure);
    }
    onSave() {
        this.env.replaceSidePanel("PivotSidePanel", `pivot_measure_display_${this.props.pivotId}_${this.props.measure.id}`, {
            pivotId: this.props.pivotId,
        });
    }
    onCancel() {
        this.store.cancelMeasureDisplayEdition();
        this.env.replaceSidePanel("PivotSidePanel", `pivot_measure_display_${this.props.pivotId}_${this.props.measure.id}`, {
            pivotId: this.props.pivotId,
        });
    }
    get fieldChoices() {
        return this.store.fields.map((field) => ({
            value: field.nameWithGranularity,
            label: field.displayName,
        }));
    }
}

function getPivotHighlights(getters, pivotId) {
    const sheetId = getters.getActiveSheetId();
    const pivotCellPositions = getVisiblePivotCellPositions(getters, pivotId);
    const mergedZones = mergeContiguousZones(pivotCellPositions.map(positionToZone));
    return mergedZones.map((zone) => ({
        range: getters.getRangeFromZone(sheetId, zone),
        noFill: true,
        color: HIGHLIGHT_COLOR,
    }));
}
function getVisiblePivotCellPositions(getters, pivotId) {
    const positions = [];
    const sheetId = getters.getActiveSheetId();
    for (const col of getters.getSheetViewVisibleCols()) {
        for (const row of getters.getSheetViewVisibleRows()) {
            const position = { sheetId, col, row };
            const cellPivotId = getters.getPivotIdFromPosition(position);
            if (pivotId === cellPivotId) {
                positions.push(position);
            }
        }
    }
    return positions;
}

css /* scss */ `
  .pivot-defer-update {
    min-height: 40px;
  }
`;
class PivotDeferUpdate extends Component {
    static template = "o-spreadsheet-PivotDeferUpdate";
    static props = {
        deferUpdate: Boolean,
        isDirty: Boolean,
        toggleDeferUpdate: Function,
        discard: Function,
        apply: Function,
    };
    static components = {
        Section,
        Checkbox,
    };
    get deferUpdatesLabel() {
        return _t("Defer updates");
    }
    get deferUpdatesTooltip() {
        return _t("Changing the pivot definition requires to reload the data. It may take some time.");
    }
}

class PivotCustomGroupsCollapsible extends Component {
    static template = "o-spreadsheet-PivotCustomGroupsCollapsible";
    static props = {
        pivotId: String,
        customField: Object,
        onCustomFieldUpdated: Function,
    };
    static components = { SidePanelCollapsible, TextInput, Checkbox };
    get groups() {
        return this.props.customField.groups.sort((a, b) => {
            if (!a.isOtherGroup && !b.isOtherGroup) {
                return 0;
            }
            return a.isOtherGroup ? 1 : -1;
        });
    }
    get hasOthersGroup() {
        return this.props.customField.groups.some((group) => group.isOtherGroup);
    }
    addOthersGroup() {
        if (this.hasOthersGroup) {
            return;
        }
        const newGroup = {
            name: getUniquePivotGroupName(_t("Others"), this.props.customField),
            values: [],
            isOtherGroup: true,
        };
        const groups = [...this.props.customField.groups, newGroup];
        this.updateCustomField({ ...this.props.customField, groups });
    }
    onDeleteGroup(groupIndex) {
        const groups = [...this.props.customField.groups];
        groups.splice(groupIndex, 1);
        this.updateCustomField({ ...this.props.customField, groups });
    }
    onRenameGroup(groupIndex, newName) {
        const groups = deepCopy(this.props.customField.groups);
        const group = groups[groupIndex];
        if (group) {
            group.name = getUniquePivotGroupName(newName, this.props.customField);
            this.updateCustomField({ ...this.props.customField, groups });
        }
    }
    updateCustomField(customField) {
        const definition = this.env.model.getters.getPivotCoreDefinition(this.props.pivotId);
        this.props.onCustomFieldUpdated({
            customFields: { ...definition.customFields, [customField.name]: customField },
        });
    }
}

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

css /* scss */ `
  input.pivot-dimension-search-field:focus {
    outline: none;
  }
  .pivot-dimension-search-field-icon svg {
    width: 13px;
    height: 13px;
  }
  .pivot-dimension-search {
    background-color: white;
  }
  .add-dimension.o-button {
    padding: 2px 7px;
    font-weight: 400;
    font-size: 12px;
    flex-grow: 0;
    height: inherit;
  }
`;
class AddDimensionButton extends Component {
    static template = "o-spreadsheet-AddDimensionButton";
    static components = { Popover, TextValueProvider };
    static props = {
        onFieldPicked: Function,
        fields: Array,
        slots: { type: Object, optional: true },
    };
    buttonRef = useRef("button");
    popover = useState({ isOpen: false });
    search = useState({ input: "" });
    autoComplete;
    // TODO navigation keys. (this looks a lot like auto-complete list. Could maybe be factorized)
    setup() {
        this.autoComplete = useLocalStore(AutoCompleteStore);
        this.autoComplete.useProvider(this.getProvider());
        useExternalListener(window, "click", (ev) => {
            if (ev.target !== this.buttonRef.el) {
                this.popover.isOpen = false;
            }
        });
        useAutofocus({ refName: "autofocus" });
    }
    getProvider() {
        return {
            proposals: this.proposals,
            autoSelectFirstProposal: false,
            selectProposal: (value) => {
                const field = this.props.fields.find((field) => field.string === value);
                if (field) {
                    this.pickField(field);
                }
            },
        };
    }
    get proposals() {
        let fields;
        if (this.search.input) {
            fields = fuzzyLookup(this.search.input, this.props.fields, (field) => field.string);
        }
        else {
            fields = this.props.fields;
        }
        return fields.map((field) => {
            const text = field.string;
            return {
                text,
                fuzzySearchKey: text,
                htmlContent: getHtmlContentFromPattern(this.search.input, text, COMPOSER_ASSISTANT_COLOR, "o-semi-bold"),
            };
        });
    }
    get popoverProps() {
        const { x, y, width, height } = this.buttonRef.el.getBoundingClientRect();
        return {
            anchorRect: { x, y, width, height },
            positioning: "bottom-left",
        };
    }
    updateSearch(searchInput) {
        this.search.input = searchInput;
        this.autoComplete.useProvider(this.getProvider());
    }
    pickField(field) {
        this.props.onFieldPicked(field.name);
        this.togglePopover();
    }
    togglePopover() {
        this.popover.isOpen = !this.popover.isOpen;
        this.search.input = "";
        this.autoComplete.useProvider(this.getProvider());
    }
    onKeyDown(ev) {
        switch (ev.key) {
            case "Enter":
                const proposals = this.autoComplete.provider?.proposals;
                if (proposals?.length === 1) {
                    this.autoComplete.provider?.selectProposal(proposals[0].text || "");
                }
                const proposal = this.autoComplete.selectedProposal;
                this.autoComplete.provider?.selectProposal(proposal?.text || "");
                break;
            case "ArrowUp":
            case "ArrowDown":
                this.autoComplete.moveSelection(ev.key === "ArrowDown" ? "next" : "previous");
                break;
            case "Escape":
                this.popover.isOpen = false;
                break;
        }
    }
}

// don't use bg-white since it's flipped to dark in dark mode and we don't support dark mode
css /* scss */ `
  .pivot-dimension {
    background-color: white;
    border: 1px solid ${GRAY_300};
    border-radius: 4px;

    select.o-input {
      height: inherit;
    }

    select > option {
      background-color: white;
    }

    .pivot-dim-operator-label {
      min-width: 120px;
    }

    &.pivot-dimension-invalid {
      background-color: #ffdddd;
      border-color: red !important;
      select,
      input {
        background-color: #ffdddd;
      }
    }
  }
`;
class PivotDimension extends Component {
    static template = "o-spreadsheet-PivotDimension";
    static props = {
        dimension: Object,
        onRemoved: { type: Function, optional: true },
        onNameUpdated: { type: Function, optional: true },
        slots: { type: Object, optional: true },
    };
    static components = { CogWheelMenu, TextInput };
    updateName(name) {
        this.props.onNameUpdated?.(this.props.dimension, name === "" || name.startsWith("=") ? undefined : name);
    }
    get dimensionDisplayName() {
        const displayName = this.props.dimension.displayName;
        return collapseHierarchicalDisplayName(displayName);
    }
}

class PivotDimensionGranularity extends Component {
    static template = "o-spreadsheet-PivotDimensionGranularity";
    static props = {
        dimension: Object,
        onUpdated: Function,
        availableGranularities: Set,
        allGranularities: Array,
    };
    periods = ALL_PERIODS;
}

class PivotDimensionOrder extends Component {
    static template = "o-spreadsheet-PivotDimensionOrder";
    static props = {
        dimension: Object,
        onUpdated: Function,
    };
}

function createMeasureAutoComplete(pivot, forComputedMeasure) {
    return {
        sequence: 0,
        autoSelectFirstProposal: true,
        getProposals(tokenAtCursor) {
            const measureProposals = pivot.measures
                .filter((m) => m !== forComputedMeasure)
                .map((measure) => {
                const text = getCanonicalSymbolName(measure.id);
                return {
                    text: text,
                    description: measure.displayName,
                    htmlContent: [{ value: text, color: PIVOT_TOKEN_COLOR }],
                    fuzzySearchKey: measure.displayName + text + measure.fieldName,
                };
            });
            const dimensionsProposals = pivot.rows.concat(pivot.columns).map((dimension) => {
                const text = getCanonicalSymbolName(dimension.nameWithGranularity);
                return {
                    text: text,
                    description: dimension.displayName,
                    htmlContent: [{ value: text, color: PIVOT_TOKEN_COLOR }],
                    fuzzySearchKey: dimension.displayName + text + dimension.fieldName,
                };
            });
            return measureProposals.concat(dimensionsProposals);
        },
        selectProposal(tokenAtCursor, value) {
            let start = tokenAtCursor.end;
            if (tokenAtCursor.type === "SYMBOL") {
                start = tokenAtCursor.start;
            }
            const end = tokenAtCursor.end;
            this.composer.changeComposerCursorSelection(start, end);
            this.composer.replaceComposerCursorSelection(value);
        },
    };
}

class PivotMeasureEditor extends Component {
    static template = "o-spreadsheet-PivotMeasureEditor";
    static components = {
        PivotDimension,
        StandaloneComposer,
    };
    static props = {
        definition: Object,
        measure: Object,
        onMeasureUpdated: Function,
        onRemoved: Function,
        generateMeasureId: Function,
        aggregators: Object,
        pivotId: String,
    };
    getMeasureAutocomplete() {
        return createMeasureAutoComplete(this.props.definition, this.props.measure);
    }
    updateMeasureFormula(formula) {
        this.props.onMeasureUpdated({
            ...this.props.measure,
            computedBy: {
                sheetId: this.env.model.getters.getActiveSheetId(),
                formula: formula[0] === "=" ? formula : "=" + formula,
            },
        });
    }
    updateAggregator(aggregator) {
        this.props.onMeasureUpdated({
            ...this.props.measure,
            aggregator,
            id: this.props.generateMeasureId(this.props.measure.fieldName, aggregator),
        });
    }
    updateName(measure, userDefinedName) {
        if (this.props.measure.computedBy && userDefinedName) {
            this.props.onMeasureUpdated({
                ...this.props.measure,
                userDefinedName,
                id: this.props.generateMeasureId(userDefinedName, this.props.measure.aggregator),
                fieldName: userDefinedName,
            });
        }
        else {
            this.props.onMeasureUpdated({
                ...this.props.measure,
                userDefinedName,
            });
        }
    }
    toggleMeasureVisibility() {
        this.props.onMeasureUpdated({
            ...this.props.measure,
            isHidden: !this.props.measure.isHidden,
        });
    }
    openShowValuesAs() {
        this.env.replaceSidePanel("PivotMeasureDisplayPanel", `pivot_key_${this.props.pivotId}`, {
            pivotId: this.props.pivotId,
            measure: this.props.measure,
        });
    }
    getColoredSymbolToken(token) {
        if (token.type !== "SYMBOL") {
            return undefined;
        }
        const tokenValue = unquote(token.value, "'");
        if (this.props.definition.columns.some((col) => col.nameWithGranularity === tokenValue) ||
            this.props.definition.rows.some((row) => row.nameWithGranularity === tokenValue) ||
            this.props.definition.measures.some((measure) => measure.id === tokenValue && measure.id !== this.props.measure.id)) {
            return PIVOT_TOKEN_COLOR;
        }
        return undefined;
    }
    get isCalculatedMeasureInvalid() {
        return compile(this.props.measure.computedBy?.formula ?? "").isBadExpression;
    }
}

css /* scss */ `
  .o-pivot-sort {
    .o-sort-card {
      width: fit-content;
      background-color: ${GRAY_100};
      border: 1px solid ${GRAY_300};

      .o-sort-value {
        color: ${PRIMARY_BUTTON_BG};
      }
    }
  }
`;
class PivotSortSection extends Component {
    static template = "o-spreadsheet-PivotSortSection";
    static components = {
        Section,
    };
    static props = {
        definition: Object,
        pivotId: String,
    };
    get hasValidSort() {
        const pivot = this.env.model.getters.getPivot(this.props.pivotId);
        return (!!this.props.definition.sortedColumn &&
            isSortedColumnValid(this.props.definition.sortedColumn, pivot));
    }
    get sortDescription() {
        const sortOrder = this.props.definition.sortedColumn?.order === "asc" ? _t("ascending") : _t("descending");
        return _t("Sorted on column (%(ascOrDesc)s):", {
            ascOrDesc: sortOrder,
        });
    }
    get sortValuesAndFields() {
        const sortedColumn = this.props.definition.sortedColumn;
        if (!sortedColumn) {
            return [];
        }
        const pivot = this.env.model.getters.getPivot(this.props.pivotId);
        const locale = this.env.model.getters.getLocale();
        const currentDomain = [];
        const sortValues = [];
        for (const domainItem of sortedColumn.domain) {
            currentDomain.push(domainItem);
            const { value, format } = pivot.getPivotHeaderValueAndFormat(currentDomain);
            const label = formatValue(value, { format, locale });
            const field = pivot.definition.getDimension(domainItem.field);
            sortValues.push({ field: getFieldDisplayName(field), value: label });
        }
        if (sortedColumn.domain.length === 0) {
            sortValues.push({ value: _t("Total") });
        }
        const measureLabel = pivot.getMeasure(sortedColumn.measure).displayName;
        sortValues.push({ value: measureLabel, field: _t("Measure") });
        return sortValues;
    }
}

css /* scss */ `
  .add-calculated-measure {
    cursor: pointer;
  }
`;
class PivotLayoutConfigurator extends Component {
    static template = "o-spreadsheet-PivotLayoutConfigurator";
    static components = {
        AddDimensionButton,
        PivotDimension,
        PivotDimensionOrder,
        PivotDimensionGranularity,
        PivotMeasureEditor,
        PivotSortSection,
        PivotCustomGroupsCollapsible,
    };
    static props = {
        definition: Object,
        onDimensionsUpdated: Function,
        unusedGroupableFields: Array,
        measureFields: Array,
        unusedGranularities: Object,
        dateGranularities: Array,
        datetimeGranularities: Array,
        getScrollableContainerEl: { type: Function, optional: true },
        pivotId: String,
    };
    dimensionsRef = useRef("pivot-dimensions");
    dragAndDrop = useDragAndDropListItems();
    AGGREGATORS = AGGREGATORS;
    composerFocus;
    isDateOrDatetimeField = isDateOrDatetimeField;
    setup() {
        this.composerFocus = useStore(ComposerFocusStore);
    }
    startDragAndDrop(dimension, event) {
        if (event.button !== 0 || event.target.tagName === "SELECT") {
            return;
        }
        const rects = this.getDimensionElementsRects();
        const definition = this.props.definition;
        const { columns, rows } = definition;
        const draggableIds = [
            ...columns.map((col) => col.nameWithGranularity),
            "__rows_title__",
            ...rows.map((row) => row.nameWithGranularity),
        ];
        const allDimensions = columns.concat(rows);
        const offset = 1; // column title
        const draggableItems = draggableIds.map((id, index) => ({
            id,
            size: rects[index + offset].height,
            position: rects[index + offset].y,
        }));
        this.dragAndDrop.start("vertical", {
            draggedItemId: dimension.nameWithGranularity,
            initialMousePosition: event.clientY,
            items: draggableItems,
            scrollableContainerEl: this.props.getScrollableContainerEl?.() || this.dimensionsRef.el,
            onDragEnd: (dimensionName, finalIndex) => {
                const originalIndex = draggableIds.findIndex((id) => id === dimensionName);
                if (originalIndex === finalIndex) {
                    return;
                }
                const draggedItems = [...draggableIds];
                draggedItems.splice(originalIndex, 1);
                draggedItems.splice(finalIndex, 0, dimensionName);
                const columns = draggedItems.slice(0, draggedItems.indexOf("__rows_title__"));
                const rows = draggedItems.slice(draggedItems.indexOf("__rows_title__") + 1);
                this.props.onDimensionsUpdated({
                    columns: columns
                        .map((nameWithGranularity) => allDimensions.find((dimension) => dimension.nameWithGranularity === nameWithGranularity))
                        .filter(isDefined),
                    rows: rows
                        .map((nameWithGranularity) => allDimensions.find((dimension) => dimension.nameWithGranularity === nameWithGranularity))
                        .filter(isDefined),
                });
            },
        });
    }
    getGranularitiesFor(field) {
        if (!isDateOrDatetimeField(field)) {
            return [];
        }
        return field.type === "date" ? this.props.dateGranularities : this.props.datetimeGranularities;
    }
    startDragAndDropMeasures(measure, event) {
        if (event.button !== 0 ||
            event.target.tagName === "SELECT" ||
            event.target.tagName === "INPUT" ||
            this.composerFocus.focusMode !== "inactive") {
            return;
        }
        const rects = this.getDimensionElementsRects();
        const definition = this.props.definition;
        const { measures, columns, rows } = definition;
        const draggableIds = measures.map((m) => m.id);
        const offset = 3 + columns.length + rows.length; // column title, row title, measure title
        const draggableItems = draggableIds.map((id, index) => ({
            id,
            size: rects[index + offset].height,
            position: rects[index + offset].y,
        }));
        this.dragAndDrop.start("vertical", {
            draggedItemId: measure.id,
            initialMousePosition: event.clientY,
            items: draggableItems,
            scrollableContainerEl: this.props.getScrollableContainerEl?.() || this.dimensionsRef.el,
            onDragEnd: (measureName, finalIndex) => {
                const originalIndex = draggableIds.findIndex((id) => id === measureName);
                if (originalIndex === finalIndex) {
                    return;
                }
                const draggedItems = [...draggableIds];
                draggedItems.splice(originalIndex, 1);
                draggedItems.splice(finalIndex, 0, measureName);
                this.props.onDimensionsUpdated({
                    measures: draggedItems
                        .map((measureId) => measures.find((measure) => measure.id === measureId))
                        .filter(isDefined),
                });
            },
        });
    }
    getDimensionElementsRects() {
        return Array.from(this.dimensionsRef.el.children).map((el) => {
            const style = getComputedStyle(el);
            const rect = el.getBoundingClientRect();
            return {
                x: rect.x,
                y: rect.y,
                width: rect.width + parseInt(style.marginLeft || "0") + parseInt(style.marginRight || "0"),
                height: rect.height + parseInt(style.marginTop || "0") + parseInt(style.marginBottom || "0"),
            };
        });
    }
    removeDimension(dimension) {
        const { columns, rows } = this.props.definition;
        this.props.onDimensionsUpdated({
            columns: columns.filter((col) => col.nameWithGranularity !== dimension.nameWithGranularity),
            rows: rows.filter((row) => row.nameWithGranularity !== dimension.nameWithGranularity),
        });
    }
    removeMeasureDimension(measure) {
        const { measures } = this.props.definition;
        this.props.onDimensionsUpdated({
            measures: measures.filter((m) => m.id !== measure.id),
        });
    }
    addColumnDimension(fieldName) {
        const { columns } = this.props.definition;
        this.props.onDimensionsUpdated({
            columns: columns.concat([{ fieldName: fieldName, order: "asc" }]),
        });
    }
    addRowDimension(fieldName) {
        const { rows } = this.props.definition;
        this.props.onDimensionsUpdated({
            rows: rows.concat([{ fieldName: fieldName, order: "asc" }]),
        });
    }
    addMeasureDimension(fieldName) {
        const { measures } = this.props.definition;
        const aggregator = this.getDefaultMeasureAggregator(fieldName);
        this.props.onDimensionsUpdated({
            measures: measures.concat([
                { id: this.getMeasureId(fieldName, aggregator), fieldName, aggregator },
            ]),
        });
    }
    updateMeasure(measure, newMeasure) {
        const { measures } = this.props.definition;
        const update = {
            measures: measures.map((m) => (m.id === measure.id ? newMeasure : m)),
        };
        if (this.props.definition.sortedColumn?.measure === measure.id) {
            update.sortedColumn = { ...this.props.definition.sortedColumn, measure: newMeasure.id };
        }
        this.props.onDimensionsUpdated(update);
    }
    getMeasureId(fieldName, aggregator) {
        const baseId = fieldName.replaceAll("'", "") + (aggregator ? `:${aggregator}` : "");
        let id = baseId;
        let i = 2;
        while (this.props.definition.measures.some((m) => m.id === id)) {
            id = `${baseId}:${i}`;
            i++;
        }
        return id;
    }
    getDefaultMeasureAggregator(fieldName) {
        const field = this.props.measureFields.find((f) => f.name === fieldName);
        return field?.aggregator ? field.aggregator : "count";
    }
    addCalculatedMeasure() {
        const { measures } = this.props.definition;
        const measureName = this.env.model.getters.generateNewCalculatedMeasureName(measures);
        const aggregator = "sum";
        this.props.onDimensionsUpdated({
            measures: measures.concat([
                {
                    id: this.getMeasureId(measureName, aggregator),
                    fieldName: measureName,
                    aggregator,
                    computedBy: {
                        sheetId: this.env.model.getters.getActiveSheetId(),
                        formula: "=0",
                    },
                },
            ]),
        });
    }
    getCustomField(dimension) {
        const definition = this.env.model.getters.getPivotCoreDefinition(this.props.pivotId);
        return definition.customFields?.[dimension.nameWithGranularity];
    }
    updateOrder(updateDimension, order) {
        const { rows, columns } = this.props.definition;
        this.props.onDimensionsUpdated({
            rows: rows.map((row) => {
                if (row.nameWithGranularity === updateDimension.nameWithGranularity) {
                    return { ...row, order: order || undefined };
                }
                return row;
            }),
            columns: columns.map((col) => {
                if (col.nameWithGranularity === updateDimension.nameWithGranularity) {
                    return { ...col, order: order || undefined };
                }
                return col;
            }),
        });
    }
    updateGranularity(dimension, granularity) {
        const { rows, columns } = this.props.definition;
        this.props.onDimensionsUpdated({
            rows: rows.map((row) => {
                if (row.nameWithGranularity === dimension.nameWithGranularity) {
                    return { ...row, granularity };
                }
                return row;
            }),
            columns: columns.map((col) => {
                if (col.nameWithGranularity === dimension.nameWithGranularity) {
                    return { ...col, granularity };
                }
                return col;
            }),
        });
    }
    getMeasureDescription(measure) {
        const measureDisplay = measure.display;
        if (!measureDisplay || measureDisplay.type === "no_calculations") {
            return "";
        }
        const pivot = this.env.model.getters.getPivot(this.props.pivotId);
        const field = [...pivot.definition.columns, ...pivot.definition.rows].find((f) => f.nameWithGranularity === measureDisplay.fieldNameWithGranularity);
        const fieldName = field ? getFieldDisplayName(field) : "";
        return measureDisplayTerms.descriptions[measureDisplay.type](fieldName);
    }
    getHugeDimensionErrorMessage(dimension) {
        const pivot = this.env.model.getters.getPivot(this.props.pivotId);
        const possibleValues = pivot.getPossibleFieldValues(dimension);
        return possibleValues.length > 100
            ? _t("This dimension contains a lot of values (%s), and might slow down the pivot table.", possibleValues.length)
            : undefined;
    }
}

class PivotTitleSection extends Component {
    static template = "o-spreadsheet-PivotTitleSection";
    static components = { CogWheelMenu, Section, TextInput };
    static props = {
        pivotId: String,
        flipAxis: Function,
    };
    get cogWheelMenuItems() {
        return [
            {
                name: _t("Flip axes"),
                icon: "o-spreadsheet-Icon.EXCHANGE",
                execute: this.props.flipAxis,
            },
            {
                name: _t("Duplicate"),
                icon: "o-spreadsheet-Icon.COPY",
                execute: () => this.duplicatePivot(),
            },
            {
                name: _t("Delete"),
                icon: "o-spreadsheet-Icon.TRASH",
                execute: () => this.delete(),
            },
        ];
    }
    get name() {
        return this.env.model.getters.getPivotName(this.props.pivotId);
    }
    get displayName() {
        return this.env.model.getters.getPivotDisplayName(this.props.pivotId);
    }
    duplicatePivot() {
        const newPivotId = this.env.model.uuidGenerator.smallUuid();
        const newSheetId = this.env.model.uuidGenerator.smallUuid();
        const result = this.env.model.dispatch("DUPLICATE_PIVOT_IN_NEW_SHEET", {
            pivotId: this.props.pivotId,
            newPivotId,
            newSheetId,
        });
        let text;
        if (result.isSuccessful) {
            text = _t("Pivot duplicated.");
        }
        else if (result.isCancelledBecause("PivotInError" /* CommandResult.PivotInError */)) {
            text = _t("Cannot duplicate a pivot in error.");
        }
        else {
            text = _t("Pivot duplication failed.");
        }
        const type = result.isSuccessful ? "success" : "danger";
        this.env.notifyUser({
            text,
            sticky: false,
            type,
        });
        if (result.isSuccessful) {
            this.env.openSidePanel("PivotSidePanel", { pivotId: newPivotId });
        }
    }
    delete() {
        this.env.askConfirmation(_t("Are you sure you want to delete this pivot?"), () => {
            this.env.model.dispatch("REMOVE_PIVOT", { pivotId: this.props.pivotId });
        });
    }
    onNameChanged(name) {
        const pivot = this.env.model.getters.getPivotCoreDefinition(this.props.pivotId);
        this.env.model.dispatch("UPDATE_PIVOT", {
            pivotId: this.props.pivotId,
            pivot: {
                ...pivot,
                name,
            },
        });
    }
}

const PIVOT_FUNCTIONS = ["PIVOT.VALUE", "PIVOT.HEADER", "PIVOT"];
/**
 * Create a proposal entry for the composer autocomplete
 * to insert a field name string in a formula.
 */
function makeFieldProposal(field, granularity) {
    const groupBy = granularity ? `${field.name}:${granularity}` : field.name;
    const quotedGroupBy = `"${groupBy}"`;
    const fuzzySearchKey = field.string !== field.name
        ? field.string + quotedGroupBy // search on translated name and on technical name
        : quotedGroupBy;
    return {
        text: quotedGroupBy,
        description: field.string + (field.help ? ` (${field.help})` : ""),
        htmlContent: [{ value: quotedGroupBy, color: tokenColors.STRING }],
        fuzzySearchKey,
    };
}
function makeMeasureProposal(measure) {
    const quotedMeasure = `"${measure.id}"`;
    const fuzzySearchKey = measure.displayName + measure.fieldName + quotedMeasure;
    return {
        text: quotedMeasure,
        description: measure.displayName,
        htmlContent: [{ value: quotedMeasure, color: tokenColors.STRING }],
        fuzzySearchKey,
    };
}
/**
 * Perform the autocomplete of the composer by inserting the value
 * at the cursor position, replacing the current token if necessary.
 * Must be bound to the autocomplete provider.
 */
function insertTokenAfterArgSeparator(tokenAtCursor, value) {
    let start = tokenAtCursor.end;
    const end = tokenAtCursor.end;
    if (tokenAtCursor.type !== "ARG_SEPARATOR") {
        // replace the whole token
        start = tokenAtCursor.start;
    }
    this.composer.changeComposerCursorSelection(start, end);
    this.composer.replaceComposerCursorSelection(value);
}
/**
 * Perform the autocomplete of the composer by inserting the value
 * at the cursor position, replacing the current token if necessary.
 * Must be bound to the autocomplete provider.
 * @param {EnrichedToken} tokenAtCursor
 * @param {string} value
 */
function insertTokenAfterLeftParenthesis(tokenAtCursor, value) {
    let start = tokenAtCursor.end;
    const end = tokenAtCursor.end;
    if (tokenAtCursor.type !== "LEFT_PAREN") {
        // replace the whole token
        start = tokenAtCursor.start;
    }
    this.composer.changeComposerCursorSelection(start, end);
    this.composer.replaceComposerCursorSelection(value);
}
/**
 * Extract the pivot id (always the first argument) from the function
 * context of the given token.
 */
function extractFormulaIdFromToken(tokenAtCursor) {
    const idAst = tokenAtCursor.functionContext?.args[0];
    if (!idAst || !["STRING", "NUMBER"].includes(idAst.type)) {
        return;
    }
    return idAst.value;
}
/**
 * Get the first Pivot function description of the given formula.
 */
function getFirstPivotFunction(tokens) {
    return getFunctionsFromTokens(tokens, PIVOT_FUNCTIONS)[0];
}
/**
 * Parse a spreadsheet formula and detect the number of PIVOT functions that are
 * present in the given formula.
 */
function getNumberOfPivotFunctions(tokens) {
    return getFunctionsFromTokens(tokens, PIVOT_FUNCTIONS).length;
}

class PivotSidePanelStore extends SpreadsheetStore {
    pivotId;
    mutators = ["reset", "deferUpdates", "applyUpdate", "discardPendingUpdate", "update"];
    updatesAreDeferred;
    draft = null;
    notification = this.get(NotificationStore);
    alreadyNotified = false;
    alreadyNotifiedForPivotSize = false;
    constructor(get, pivotId) {
        super(get);
        this.pivotId = pivotId;
        this.updatesAreDeferred =
            this.getters.getPivotCoreDefinition(this.pivotId).deferUpdates ?? false;
    }
    handle(cmd) {
        switch (cmd.type) {
            case "UPDATE_PIVOT":
                if (cmd.pivotId === this.pivotId) {
                    this.getters.getPivot(this.pivotId).init();
                }
        }
    }
    get fields() {
        return this.pivot.getFields();
    }
    get pivot() {
        return this.getters.getPivot(this.pivotId);
    }
    get definition() {
        const Definition = pivotRegistry.get(this.pivot.type).definition;
        return this.draft
            ? new Definition(this.draft, this.fields, this.getters)
            : this.pivot.definition;
    }
    get isDirty() {
        return !!this.draft;
    }
    get measureFields() {
        const measureFields = [
            {
                name: "__count",
                string: _t("Count"),
                type: "integer",
                aggregator: "sum",
            },
        ];
        const fields = this.fields;
        for (const fieldName in fields) {
            const field = fields[fieldName];
            if (!field) {
                continue;
            }
            if (pivotRegistry.get(this.pivot.type).isMeasureCandidate(field)) {
                measureFields.push(field);
            }
        }
        return measureFields.sort((a, b) => a.string.localeCompare(b.string));
    }
    get unusedGroupableFields() {
        const groupableFields = [];
        const fields = this.fields;
        for (const fieldName in fields) {
            const field = fields[fieldName];
            if (!field) {
                continue;
            }
            if (pivotRegistry.get(this.pivot.type).isGroupable(field)) {
                groupableFields.push(field);
            }
        }
        const { columns, rows, measures } = this.definition;
        const currentlyUsed = measures
            .concat(rows)
            .concat(columns)
            .map((field) => field.fieldName);
        const unusedGranularities = this.unusedGranularities;
        return groupableFields
            .filter((field) => {
            if (isDateOrDatetimeField(field)) {
                return !currentlyUsed.includes(field.name) || unusedGranularities[field.name].size > 0;
            }
            return !currentlyUsed.includes(field.name);
        })
            .sort((a, b) => a.string.localeCompare(b.string));
    }
    get datetimeGranularities() {
        return pivotRegistry.get(this.pivot.type).datetimeGranularities;
    }
    get dateGranularities() {
        return pivotRegistry.get(this.pivot.type).dateGranularities;
    }
    get unusedGranularities() {
        return this.getUnusedGranularities(this.fields, this.draft ?? this.getters.getPivotCoreDefinition(this.pivotId));
    }
    reset(pivotId) {
        this.pivotId = pivotId;
        this.updatesAreDeferred = true;
        this.draft = null;
    }
    deferUpdates(shouldDefer) {
        if (shouldDefer === false && this.draft) {
            this.draft.deferUpdates = false;
            this.applyUpdate();
        }
        else {
            this.update({ deferUpdates: shouldDefer });
        }
        this.updatesAreDeferred = shouldDefer;
    }
    applyUpdate() {
        if (this.draft) {
            this.model.dispatch("UPDATE_PIVOT", {
                pivotId: this.pivotId,
                pivot: this.draft,
            });
            this.draft = null;
            if (!this.alreadyNotified && this.isUpdatedPivotVisibleInViewportOnlyAsStaticPivot()) {
                const formulaId = this.getters.getPivotFormulaId(this.pivotId);
                const pivotExample = `=PIVOT(${formulaId})`;
                this.alreadyNotified = true;
                this.notification.notifyUser({
                    type: "info",
                    text: _t("Pivot updates only work with dynamic pivot tables. Use the formula '%s' or re-insert the static pivot from the Data menu.", pivotExample),
                    sticky: true,
                });
            }
            const pivot = this.getters.getPivot(this.pivotId);
            const numberOfCells = pivot.isValid() ? pivot.getExpandedTableStructure().numberOfCells : 0;
            if (!this.alreadyNotifiedForPivotSize && numberOfCells > PIVOT_MAX_NUMBER_OF_CELLS) {
                this.alreadyNotifiedForPivotSize = true;
                this.notification.notifyUser({
                    type: "warning",
                    text: getPivotTooBigErrorMessage(numberOfCells, this.getters.getLocale()),
                    sticky: true,
                });
            }
        }
    }
    discardPendingUpdate() {
        this.draft = null;
    }
    update(definitionUpdate) {
        const coreDefinition = this.getters.getPivotCoreDefinition(this.pivotId);
        const definition = {
            ...coreDefinition,
            ...this.draft,
            ...definitionUpdate,
        };
        // clean to make sure we only keep the core properties
        const cleanedDefinition = {
            ...definition,
            columns: definition.columns.map((col) => ({
                fieldName: col.fieldName,
                order: col.order,
                granularity: col.granularity,
            })),
            rows: definition.rows.map((row) => ({
                fieldName: row.fieldName,
                order: row.order,
                granularity: row.granularity,
            })),
            measures: definition.measures.map((measure) => ({
                id: measure.id,
                fieldName: measure.fieldName,
                aggregator: measure.aggregator,
                userDefinedName: measure.userDefinedName,
                computedBy: measure.computedBy,
                isHidden: measure.isHidden,
                format: measure.format,
                display: measure.display,
            })),
            sortedColumn: this.shouldKeepSortedColumn(definition) ? definition.sortedColumn : undefined,
        };
        if (cleanedDefinition.collapsedDomains) {
            const { COL, ROW } = cleanedDefinition.collapsedDomains;
            cleanedDefinition.collapsedDomains = {
                COL: COL.filter((domain) => this.areDomainFieldsValid(domain, cleanedDefinition.columns)),
                ROW: ROW.filter((domain) => this.areDomainFieldsValid(domain, cleanedDefinition.rows)),
            };
        }
        if (!this.draft && deepEquals(coreDefinition, cleanedDefinition)) {
            return;
        }
        const cleanedWithGranularity = this.addDefaultDateTimeGranularity(this.fields, cleanedDefinition);
        this.draft = cleanedWithGranularity;
        if (!this.updatesAreDeferred) {
            this.applyUpdate();
        }
    }
    /**
     * @returns true if the updated pivot is visible in the viewport only as a
     * static pivot and not as a dynamic pivot
     */
    isUpdatedPivotVisibleInViewportOnlyAsStaticPivot() {
        let staticPivotCount = 0;
        const updatedPivotFormulaId = this.getters.getPivotFormulaId(this.pivotId);
        for (const position of this.getters.getVisibleCellPositions()) {
            const cell = this.getters.getCell(position);
            if (cell?.isFormula) {
                const pivotFunction = getFirstPivotFunction(cell.compiledFormula.tokens);
                const pivotFormulaId = pivotFunction?.args[0]?.value;
                if (pivotFunction && updatedPivotFormulaId === pivotFormulaId.toString()) {
                    if (pivotFunction.functionName === "PIVOT") {
                        // if we have at least one dynamic pivot visible inserted the viewport
                        // we return false
                        return false;
                    }
                    else {
                        staticPivotCount++;
                    }
                }
            }
        }
        // we return true if there are only static pivots visible inserted the viewport,
        // otherwise false
        return staticPivotCount > 0;
    }
    addDefaultDateTimeGranularity(fields, definition) {
        const { columns, rows } = definition;
        const columnsWithGranularity = deepCopy(columns);
        const rowsWithGranularity = deepCopy(rows);
        const unusedGranularities = this.getUnusedGranularities(fields, definition);
        for (const dimension of columnsWithGranularity.concat(rowsWithGranularity)) {
            const fieldType = fields[dimension.fieldName]?.type;
            if ((fieldType === "date" || fieldType === "datetime") && !dimension.granularity) {
                const granularity = unusedGranularities[dimension.fieldName]?.values().next().value || "year";
                unusedGranularities[dimension.fieldName]?.delete(granularity);
                dimension.granularity = granularity;
            }
        }
        return {
            ...definition,
            columns: columnsWithGranularity,
            rows: rowsWithGranularity,
        };
    }
    getUnusedGranularities(fields, definition) {
        const { columns, rows, measures } = definition;
        const dateFields = columns
            .concat(rows)
            .concat(measures)
            .filter((dimension) => {
            const fieldType = fields[dimension.fieldName]?.type;
            return fieldType === "date" || fieldType === "datetime";
        });
        const granularitiesPerFields = {};
        for (const field of dateFields) {
            granularitiesPerFields[field.fieldName] = new Set(fields[field.fieldName]?.type === "date"
                ? this.dateGranularities
                : this.datetimeGranularities);
        }
        for (const field of dateFields) {
            granularitiesPerFields[field.fieldName].delete(field.granularity || "month");
        }
        return granularitiesPerFields;
    }
    /**
     * Check if we want to keep the sorted column when updating the pivot definition. We should remove it if either
     * the measure is not in the new definition or the columns have changed.
     */
    shouldKeepSortedColumn(newDefinition) {
        const { sortedColumn } = newDefinition;
        if (!sortedColumn) {
            return true;
        }
        const oldDefinition = this.getters.getPivotCoreDefinition(this.pivotId);
        return (newDefinition.measures.find((measure) => measure.id === sortedColumn.measure) &&
            deepEquals(oldDefinition.columns, newDefinition.columns));
    }
    areDomainFieldsValid(domain, dims) {
        const fieldsNameWithGranularity = dims.map(({ fieldName, granularity }) => fieldName + (granularity ? `:${granularity}` : ""));
        for (let i = 0; i < domain.length; i++) {
            if (domain[i].field !== fieldsNameWithGranularity[i]) {
                return false;
            }
        }
        return true;
    }
}

class PivotSpreadsheetSidePanel extends Component {
    static template = "o-spreadsheet-PivotSpreadsheetSidePanel";
    static props = {
        pivotId: String,
        onCloseSidePanel: Function,
    };
    static components = {
        PivotLayoutConfigurator,
        Section,
        SelectionInput,
        Checkbox,
        PivotDeferUpdate,
        PivotTitleSection,
    };
    store;
    state;
    pivotSidePanelRef = useRef("pivotSidePanel");
    setup() {
        this.store = useLocalStore(PivotSidePanelStore, this.props.pivotId);
        this.state = useState({
            range: undefined,
            rangeHasChanged: false,
        });
    }
    get shouldDisplayInvalidRangeError() {
        if (this.store.isDirty && this.state.rangeHasChanged) {
            return false;
        }
        return this.pivot.isInvalidRange;
    }
    get ranges() {
        if (this.state.range) {
            return [this.state.range];
        }
        if (this.definition.range) {
            return [this.env.model.getters.getRangeString(this.definition.range, "forceSheetReference")];
        }
        return [];
    }
    get pivot() {
        return this.store.pivot;
    }
    get definition() {
        return this.store.definition;
    }
    getScrollableContainerEl() {
        return this.pivotSidePanelRef.el;
    }
    onSelectionChanged(ranges) {
        this.state.rangeHasChanged = true;
        this.state.range = ranges[0];
    }
    onSelectionConfirmed() {
        if (this.state.range) {
            const range = this.env.model.getters.getRangeFromSheetXC(this.env.model.getters.getActiveSheetId(), this.state.range);
            if (range.invalidSheetName || range.invalidXc) {
                return;
            }
            const dataSet = { sheetId: range.sheetId, zone: range.zone };
            this.store.update({ dataSet });
            // Immediately apply the update to recompute the pivot fields
            this.store.applyUpdate();
        }
    }
    flipAxis() {
        const { rows, columns } = this.definition;
        this.onDimensionsUpdated({
            rows: columns,
            columns: rows,
        });
    }
    onDimensionsUpdated(definition) {
        this.store.update(definition);
    }
}

const pivotSidePanelRegistry = new Registry();
pivotSidePanelRegistry.add("SPREADSHEET", {
    editor: PivotSpreadsheetSidePanel,
});

class PivotSidePanel extends Component {
    static template = "o-spreadsheet-PivotSidePanel";
    static props = {
        pivotId: String,
        onCloseSidePanel: Function,
    };
    static components = {
        PivotLayoutConfigurator,
        Section,
    };
    setup() {
        useHighlights(this);
    }
    get sidePanelEditor() {
        const pivot = this.env.model.getters.getPivotCoreDefinition(this.props.pivotId);
        if (!pivot) {
            throw new Error("pivotId does not correspond to a pivot.");
        }
        return pivotSidePanelRegistry.get(pivot.type).editor;
    }
    get highlights() {
        return getPivotHighlights(this.env.model.getters, this.props.pivotId);
    }
}

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

css /* scss */ `
  .o-locale-preview {
    border: 1px solid ${GRAY_300};
    background-color: ${GRAY_100};
  }
`;
class SettingsPanel extends Component {
    static template = "o-spreadsheet-SettingsPanel";
    static components = { Section, ValidationMessages, BadgeSelection };
    static props = { onCloseSidePanel: Function };
    loadedLocales = [];
    setup() {
        onWillStart(() => this.loadLocales());
    }
    onLocaleChange(code) {
        const locale = this.loadedLocales.find((l) => l.code === code);
        if (!locale)
            return;
        this.env.model.dispatch("UPDATE_LOCALE", { locale });
    }
    async loadLocales() {
        this.loadedLocales = (await this.env.loadLocales())
            .filter((locale) => {
            const isValid = isValidLocale(locale);
            if (!isValid) {
                console.warn(`Invalid locale: ${locale["code"]} ${locale}`);
            }
            return isValid;
        })
            .sort((a, b) => a.name.localeCompare(b.name));
    }
    get numberFormatPreview() {
        const locale = this.env.model.getters.getLocale();
        return formatValue(1234567.89, { format: "#,##0.00", locale });
    }
    get dateFormatPreview() {
        const locale = this.env.model.getters.getLocale();
        return formatValue(1.6, { format: locale.dateFormat, locale });
    }
    get dateTimeFormatPreview() {
        const locale = this.env.model.getters.getLocale();
        const dateTimeFormat = getDateTimeFormat(locale);
        return formatValue(1.6, { format: dateTimeFormat, locale });
    }
    get firstDayOfWeek() {
        const locale = this.env.model.getters.getLocale();
        const weekStart = locale.weekStart;
        // Week start: 1 = Monday, 7 = Sunday
        // Days: 0 = Sunday, 6 = Saturday
        return DAYS$1[weekStart % 7];
    }
    get currentLocale() {
        return this.env.model.getters.getLocale();
    }
    get supportedLocales() {
        const currentLocale = this.currentLocale;
        const localeInLoadedLocales = this.loadedLocales.find((l) => l.code === currentLocale.code);
        if (!localeInLoadedLocales) {
            return [...this.loadedLocales, currentLocale].sort((a, b) => a.name.localeCompare(b.name));
        }
        else if (!deepEquals(currentLocale, localeInLoadedLocales)) {
            const index = this.loadedLocales.indexOf(localeInLoadedLocales);
            const locales = [...this.loadedLocales];
            locales[index] = currentLocale;
            locales.sort((a, b) => a.name.localeCompare(b.name));
            return locales;
        }
        return this.loadedLocales;
    }
}

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

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

function createTableStyleContextMenuActions(env, styleId) {
    if (!env.model.getters.isTableStyleEditable(styleId)) {
        return [];
    }
    return createActions([
        {
            id: "editTableStyle",
            name: _t("Edit table style"),
            execute: (env) => env.openSidePanel("TableStyleEditorPanel", { styleId }),
            isEnabled: (env) => !env.isSmall,
            icon: "o-spreadsheet-Icon.EDIT",
        },
        {
            id: "deleteTableStyle",
            name: _t("Delete table style"),
            execute: (env) => env.model.dispatch("REMOVE_TABLE_STYLE", { tableStyleId: styleId }),
            icon: "o-spreadsheet-Icon.TRASH",
        },
    ]);
}

function drawPreviewTable(ctx, tableStyle, colWidth, rowHeight) {
    ctx.resetTransform();
    drawBackgrounds(ctx, tableStyle, colWidth, rowHeight);
    drawBorders(ctx, tableStyle, colWidth, rowHeight);
    drawTexts(ctx, tableStyle, colWidth, rowHeight);
}
function drawBackgrounds(ctx, tableStyle, colWidth, rowHeight) {
    ctx.save();
    for (let col = 0; col < 5; col++) {
        for (let row = 0; row < 5; row++) {
            ctx.fillStyle = tableStyle.styles[col][row].fillColor || "#fff";
            ctx.fillRect(col * colWidth, row * rowHeight, colWidth, rowHeight);
        }
    }
    ctx.restore();
}
function drawBorders(ctx, tableStyle, colWidth, rowHeight) {
    ctx.save();
    ctx.translate(0, 0.5);
    ctx.lineWidth = 1;
    for (let col = 0; col < 5; col++) {
        for (let row = 0; row < 5; row++) {
            const borders = tableStyle.borders[col][row];
            if (borders.top) {
                ctx.strokeStyle = borders.top.color;
                ctx.beginPath();
                ctx.moveTo(col * colWidth, row * rowHeight);
                ctx.lineTo(col * colWidth + colWidth, row * rowHeight);
                ctx.stroke();
            }
            if (borders.bottom) {
                ctx.strokeStyle = borders.bottom.color;
                ctx.beginPath();
                ctx.moveTo(col * colWidth, row * rowHeight + rowHeight);
                ctx.lineTo(col * colWidth + colWidth, row * rowHeight + rowHeight);
                ctx.stroke();
            }
        }
    }
    ctx.resetTransform();
    ctx.translate(0.5, 0);
    for (let col = 0; col < 5; col++) {
        for (let row = 0; row < 5; row++) {
            const borders = tableStyle.borders[col][row];
            if (borders.left) {
                ctx.strokeStyle = borders.left.color;
                ctx.beginPath();
                ctx.moveTo(col * colWidth, row * rowHeight);
                ctx.lineTo(col * colWidth, row * rowHeight + rowHeight);
                ctx.stroke();
            }
            if (borders.right) {
                ctx.strokeStyle = borders.right.color;
                ctx.beginPath();
                ctx.moveTo(col * colWidth + colWidth, row * rowHeight);
                ctx.lineTo(col * colWidth + colWidth, row * rowHeight + rowHeight + 1); // +1 to draw on the bottom-right pixel of the table
                ctx.stroke();
            }
        }
    }
    ctx.restore();
}
function drawTexts(ctx, tableStyle, colWidth, rowHeight) {
    ctx.save();
    ctx.translate(0, 0.5);
    ctx.lineWidth = 1;
    const xPadding = Math.floor(colWidth / 4);
    const yPadding = Math.floor(rowHeight / 2);
    for (let col = 0; col < 5; col++) {
        for (let row = 0; row < 5; row++) {
            ctx.strokeStyle = tableStyle.styles[col][row].textColor || "#000";
            ctx.beginPath();
            ctx.moveTo(col * colWidth + xPadding + 1, row * rowHeight + yPadding);
            ctx.lineTo(col * colWidth + colWidth - xPadding, row * rowHeight + yPadding);
            ctx.stroke();
        }
    }
    ctx.restore();
}

css /* scss */ `
  .o-table-style-list-item {
    border: 1px solid transparent;
    border-radius: 4px;
    &.selected {
      border: 1px solid ${ACTION_COLOR};
      background: ${BADGE_SELECTED_COLOR};
    }

    &:hover {
      background: #ddd;
      .o-table-style-edit-button {
        display: block !important;
        right: 0;
        top: 0;
        background: #fff;
        cursor: pointer;
        border: 1px solid #ddd;
        padding: 1px 1px 1px 2px;
        .o-icon {
          font-size: 12px;
          width: 12px;
          height: 12px;
        }
      }
    }
  }
`;
class TableStylePreview extends Component {
    static template = "o-spreadsheet-TableStylePreview";
    static components = { MenuPopover };
    static props = {
        tableConfig: Object,
        tableStyle: Object,
        class: String,
        styleId: { type: String, optional: true },
        selected: { type: Boolean, optional: true },
        onClick: { type: Function, optional: true },
    };
    canvasRef = useRef("canvas");
    menu = useState({ isOpen: false, anchorRect: null, menuItems: [] });
    setup() {
        onWillUpdateProps((nextProps) => {
            if (!deepEquals(this.props.tableConfig, nextProps.tableConfig) ||
                !deepEquals(this.props.tableStyle, nextProps.tableStyle)) {
                this.drawTable(nextProps);
            }
        });
        onMounted(() => this.drawTable(this.props));
    }
    drawTable(props) {
        const ctx = this.canvasRef.el.getContext("2d");
        const { width, height } = this.canvasRef.el.getBoundingClientRect();
        this.canvasRef.el.width = width;
        this.canvasRef.el.height = height;
        const computedStyle = getComputedTableStyle(props.tableConfig, props.tableStyle, 5, 5);
        drawPreviewTable(ctx, computedStyle, (width - 1) / 5, (height - 1) / 5);
    }
    onContextMenu(event) {
        if (!this.props.styleId) {
            return;
        }
        this.menu.menuItems = createTableStyleContextMenuActions(this.env, this.props.styleId);
        this.menu.isOpen = true;
        this.menu.anchorRect = { x: event.clientX, y: event.clientY, width: 0, height: 0 };
    }
    closeMenu() {
        this.menu.isOpen = false;
        this.menu.anchorRect = null;
        this.menu.menuItems = [];
    }
    get styleName() {
        if (!this.props.styleId) {
            return "";
        }
        return this.env.model.getters.getTableStyle(this.props.styleId).displayName;
    }
    get isStyleEditable() {
        if (!this.props.styleId) {
            return false;
        }
        return this.env.model.getters.isTableStyleEditable(this.props.styleId);
    }
    editTableStyle() {
        this.env.openSidePanel("TableStyleEditorPanel", { styleId: this.props.styleId });
    }
}

css /* scss */ `
  .o-table-style-popover {
    /* 7 tables preview + padding by line */
    width: calc((66px + 4px * 2) * 7 + 1.5rem * 2);
    background: #fff;
    font-size: 14px;
    user-select: none;

    .o-notebook {
      border-bottom: 1px solid ${GRAY_300};

      .o-notebook-tab {
        padding: 5px 15px;
        border: 1px solid ${GRAY_300};
        margin-bottom: -1px;
        margin-left: -1px;
        color: ${TEXT_BODY};
        cursor: pointer;
        transition: color 0.2s, border-color 0.2s;

        &.selected {
          border-bottom-color: #fff;
          border-top-color: ${PRIMARY_BUTTON_BG};
          color: ${TEXT_HEADING};
        }
      }
    }

    .o-table-style-list-item {
      padding: 3px;
    }

    .o-table-style-popover-preview {
      width: 66px;
      height: 51px;
    }

    .o-new-table-style {
      font-size: 36px;
      color: #666;
      &:hover {
        background: #f5f5f5;
      }
    }
  }
`;
class TableStylesPopover extends Component {
    static template = "o-spreadsheet-TableStylesPopover";
    static components = { Popover, TableStylePreview };
    static props = {
        tableConfig: Object,
        popoverProps: { type: Object, optional: true },
        closePopover: Function,
        onStylePicked: Function,
        selectedStyleId: { type: String, optional: true },
    };
    categories = TABLE_STYLE_CATEGORIES;
    tableStyleListRef = useRef("tableStyleList");
    state = useState({ selectedCategory: this.initialSelectedCategory });
    setup() {
        useExternalListener(window, "click", this.onExternalClick, { capture: true });
    }
    onExternalClick(ev) {
        if (this.tableStyleListRef.el && !isChildEvent(this.tableStyleListRef.el, ev)) {
            this.props.closePopover();
            ev.hasClosedTableStylesPopover = true;
        }
    }
    get displayedStyles() {
        const styles = this.env.model.getters.getTableStyles();
        return Object.keys(styles).filter((styleId) => styles[styleId].category === this.state.selectedCategory);
    }
    get initialSelectedCategory() {
        return this.props.selectedStyleId
            ? this.env.model.getters.getTableStyle(this.props.selectedStyleId).category
            : "medium";
    }
    newTableStyle() {
        this.props.closePopover();
        this.env.openSidePanel("TableStyleEditorPanel", {
            onStylePicked: this.props.onStylePicked,
        });
    }
}

css /* scss */ `
  .o-table-style-picker {
    border: 1px solid ${GRAY_300};
    border-radius: 3px;

    .o-table-style-picker-arrow {
      border-left: 1px solid ${GRAY_300};

      &:hover {
        background: #f5f5f5;
        cursor: pointer;
      }
    }

    .o-table-style-list-item {
      padding: 5px 6px;
      margin: 5px 2px;

      .o-table-style-picker-preview {
        width: 51px;
        height: 36px;
      }
    }
  }
`;
class TableStylePicker extends Component {
    static template = "o-spreadsheet-TableStylePicker";
    static components = { TableStylesPopover, TableStylePreview };
    static props = { table: Object };
    state = useState({ popoverProps: undefined });
    getDisplayedTableStyles() {
        const allStyles = this.env.model.getters.getTableStyles();
        const selectedStyleCategory = allStyles[this.props.table.config.styleId].category;
        const styles = Object.keys(allStyles).filter((key) => allStyles[key].category === selectedStyleCategory);
        const selectedStyleIndex = styles.indexOf(this.props.table.config.styleId);
        if (selectedStyleIndex === -1) {
            return selectedStyleIndex;
        }
        const index = Math.floor(selectedStyleIndex / 4) * 4;
        return styles.slice(index);
    }
    onStylePicked(styleId) {
        const sheetId = this.env.model.getters.getActiveSheetId();
        this.env.model.dispatch("UPDATE_TABLE", {
            sheetId,
            zone: this.props.table.range.zone,
            config: { styleId: styleId },
        });
        this.closePopover();
    }
    onArrowButtonClick(ev) {
        if (ev.hasClosedTableStylesPopover || this.state.popoverProps) {
            this.closePopover();
            return;
        }
        const target = ev.currentTarget;
        const { bottom, right } = target.getBoundingClientRect();
        this.state.popoverProps = {
            anchorRect: { x: right, y: bottom, width: 0, height: 0 },
            positioning: "top-right",
            verticalOffset: 0,
        };
    }
    closePopover() {
        this.state.popoverProps = undefined;
    }
}

css /* scss */ `
  .o-table-panel {
    input.o-table-n-of-headers {
      width: 14px;
      text-align: center;
    }

    .o-info-icon {
      width: 14px;
      height: 14px;
    }
  }
`;
class TablePanel extends Component {
    static template = "o-spreadsheet-TablePanel";
    static components = { TableStylePicker, SelectionInput, ValidationMessages, Checkbox, Section };
    static props = { onCloseSidePanel: Function, table: Object };
    state;
    setup() {
        const sheetId = this.env.model.getters.getActiveSheetId();
        this.state = useState({
            tableZoneErrors: [],
            tableXc: this.env.model.getters.getRangeString(this.props.table.range, sheetId),
            filtersEnabledIfPossible: this.props.table.config.hasFilters,
        });
    }
    updateHasFilters(hasFilters) {
        this.state.filtersEnabledIfPossible = hasFilters;
        this.updateTableConfig("hasFilters", hasFilters);
    }
    updateTableConfig(attName, value) {
        const sheetId = this.env.model.getters.getActiveSheetId();
        return this.env.model.dispatch("UPDATE_TABLE", {
            sheetId,
            zone: this.props.table.range.zone,
            config: { [attName]: value },
        });
    }
    updateHasHeaders(hasHeaders) {
        const numberOfHeaders = hasHeaders ? 1 : 0;
        this.updateNumberOfHeaders(numberOfHeaders);
    }
    updateTableIsDynamic(isDynamic) {
        const newTableType = isDynamic ? "dynamic" : "forceStatic";
        if (newTableType === this.props.table.type) {
            return;
        }
        const uiTable = this.env.model.getters.getTable(getTableTopLeft(this.props.table));
        if (!uiTable) {
            return;
        }
        const sheetId = this.env.model.getters.getActiveSheetId();
        const result = this.env.model.dispatch("UPDATE_TABLE", {
            sheetId,
            zone: this.props.table.range.zone,
            newTableRange: this.env.model.getters.getRangeData(uiTable.range),
            tableType: newTableType,
        });
        const updatedTable = this.env.model.getters.getCoreTable(getTableTopLeft(this.props.table));
        if (result.isSuccessful && updatedTable) {
            const newTableRange = updatedTable.range;
            this.state.tableXc = this.env.model.getters.getRangeString(newTableRange, sheetId);
            this.state.tableZoneErrors = [];
        }
    }
    onChangeNumberOfHeaders(ev) {
        const input = ev.target;
        const numberOfHeaders = parseInt(input.value);
        const result = this.updateNumberOfHeaders(numberOfHeaders);
        if (!result.isSuccessful) {
            input.value = this.props.table.config.numberOfHeaders.toString();
        }
    }
    updateNumberOfHeaders(numberOfHeaders) {
        const hasFilters = numberOfHeaders > 0 && (this.tableConfig.hasFilters || this.state.filtersEnabledIfPossible);
        return this.env.model.dispatch("UPDATE_TABLE", {
            sheetId: this.env.model.getters.getActiveSheetId(),
            zone: this.props.table.range.zone,
            config: { numberOfHeaders, hasFilters },
        });
    }
    onRangeChanged(ranges) {
        const sheetId = this.env.model.getters.getActiveSheetId();
        this.state.tableXc = ranges[0];
        const newTableRange = this.env.model.getters.getRangeFromSheetXC(sheetId, this.state.tableXc);
        this.state.tableZoneErrors = this.env.model.canDispatch("UPDATE_TABLE", {
            sheetId,
            zone: this.props.table.range.zone,
            newTableRange: this.env.model.getters.getRangeDataFromXc(sheetId, this.state.tableXc),
            tableType: this.getNewTableType(newTableRange.zone),
        }).reasons;
    }
    onRangeConfirmed() {
        const sheetId = this.env.model.getters.getActiveSheetId();
        let newRange = this.env.model.getters.getRangeFromSheetXC(sheetId, this.state.tableXc);
        if (getZoneArea(newRange.zone) === 1) {
            const extendedZone = this.env.model.getters.getContiguousZone(sheetId, newRange.zone);
            newRange = this.env.model.getters.getRangeFromZone(sheetId, extendedZone);
        }
        const newTableZone = newRange.zone;
        const oldTableZone = this.props.table.range.zone;
        const cmdToCall = newTableZone.top === oldTableZone.top && newTableZone.left === oldTableZone.left
            ? "RESIZE_TABLE"
            : "UPDATE_TABLE";
        const result = this.env.model.dispatch(cmdToCall, {
            sheetId,
            zone: this.props.table.range.zone,
            newTableRange: this.env.model.getters.getRangeData(newRange),
            tableType: this.getNewTableType(newRange.zone),
        });
        const position = { sheetId, col: newRange.zone.left, row: newRange.zone.top };
        const updatedTable = this.env.model.getters.getCoreTable(position);
        if (result.isSuccessful && updatedTable) {
            const newTopLeft = getTableTopLeft(updatedTable);
            this.env.model.selection.selectZone({
                zone: positionToZone(newTopLeft),
                cell: newTopLeft,
            });
            const newTableRange = updatedTable.range;
            this.state.tableXc = this.env.model.getters.getRangeString(newTableRange, sheetId);
        }
    }
    deleteTable() {
        const sheetId = this.env.model.getters.getActiveSheetId();
        this.env.model.dispatch("REMOVE_TABLE", {
            sheetId,
            target: [this.props.table.range.zone],
        });
    }
    getNewTableType(newTableZone) {
        if (this.props.table.type === "forceStatic") {
            return "forceStatic";
        }
        const sheetId = this.env.model.getters.getActiveSheetId();
        return this.env.model.getters.canCreateDynamicTableOnZones(sheetId, [newTableZone])
            ? "dynamic"
            : "static";
    }
    get tableConfig() {
        return this.props.table.config;
    }
    get errorMessages() {
        const cancelledReasons = this.state.tableZoneErrors || [];
        return cancelledReasons.map((error) => TableTerms.Errors[error] || TableTerms.Errors.Unexpected);
    }
    getCheckboxLabel(attName) {
        return TableTerms.Checkboxes[attName];
    }
    get canHaveFilters() {
        return this.tableConfig.numberOfHeaders > 0;
    }
    get hasFilterCheckboxTooltip() {
        return this.canHaveFilters ? undefined : TableTerms.Tooltips.filterWithoutHeader;
    }
    get canBeDynamic() {
        const sheetId = this.env.model.getters.getActiveSheetId();
        return (this.props.table.type === "dynamic" ||
            this.env.model.getters.canCreateDynamicTableOnZones(sheetId, [this.props.table.range.zone]));
    }
    get dynamicTableTooltip() {
        return TableTerms.Tooltips.isDynamic;
    }
}

css /* scss */ `
  .o-table-style-editor-panel {
    .o-table-style-list-item {
      margin: 2px 7px;
      padding: 6px 9px;

      .o-table-style-edit-template-preview {
        width: 71px;
        height: 51px;
      }
    }
  }
`;
const DEFAULT_TABLE_STYLE_COLOR = "#3C78D8";
class TableStyleEditorPanel extends Component {
    static template = "o-spreadsheet-TableStyleEditorPanel";
    static components = { Section, RoundColorPicker, TableStylePreview };
    static props = {
        onCloseSidePanel: Function,
        onStylePicked: { type: Function, optional: true },
        styleId: { type: String, optional: true },
    };
    state = useState(this.getInitialState());
    setup() {
        useExternalListener(window, "click", () => (this.state.pickerOpened = false));
    }
    getInitialState() {
        const editedStyle = this.props.styleId
            ? this.env.model.getters.getTableStyle(this.props.styleId)
            : null;
        return {
            pickerOpened: false,
            primaryColor: editedStyle?.primaryColor || DEFAULT_TABLE_STYLE_COLOR,
            selectedTemplateName: editedStyle?.templateName || "lightColoredText",
            styleName: editedStyle?.displayName || this.env.model.getters.getNewCustomTableStyleName(),
        };
    }
    togglePicker() {
        this.state.pickerOpened = !this.state.pickerOpened;
    }
    onColorPicked(color) {
        this.state.primaryColor = isColorValid(color) ? color : DEFAULT_TABLE_STYLE_COLOR;
        this.state.pickerOpened = false;
    }
    onTemplatePicked(templateName) {
        this.state.selectedTemplateName = templateName;
    }
    onConfirm() {
        const tableStyleId = this.props.styleId || this.env.model.uuidGenerator.smallUuid();
        this.env.model.dispatch("CREATE_TABLE_STYLE", {
            tableStyleId,
            tableStyleName: this.state.styleName,
            templateName: this.state.selectedTemplateName,
            primaryColor: this.state.primaryColor,
        });
        this.props.onStylePicked?.(tableStyleId);
        this.props.onCloseSidePanel();
    }
    onCancel() {
        this.props.onCloseSidePanel();
    }
    onDelete() {
        if (!this.props.styleId) {
            return;
        }
        this.env.model.dispatch("REMOVE_TABLE_STYLE", { tableStyleId: this.props.styleId });
        this.props.onCloseSidePanel();
    }
    get colorPreviewStyle() {
        return cssPropertiesToCss({ background: this.state.primaryColor });
    }
    get tableTemplates() {
        return Object.keys(TABLE_STYLES_TEMPLATES).filter((templateName) => templateName !== "none");
    }
    get previewTableConfig() {
        return {
            bandedColumns: false,
            bandedRows: true,
            firstColumn: false,
            lastColumn: false,
            numberOfHeaders: 1,
            totalRow: true,
            hasFilters: true,
            styleId: "",
        };
    }
    get selectedStyle() {
        return this.computeTableStyle(this.state.selectedTemplateName);
    }
    computeTableStyle(templateName) {
        return buildTableStyle(this.state.styleName, templateName, this.state.primaryColor);
    }
}

const sidePanelRegistry = new Registry();
sidePanelRegistry.add("ConditionalFormatting", {
    title: _t("Conditional formatting"),
    Body: ConditionalFormattingPanel,
});
sidePanelRegistry.add("ChartPanel", {
    title: _t("Chart"),
    Body: ChartPanel,
    computeState: (getters, initialProps) => {
        const figureId = getters.getSelectedFigureId();
        const chartId = figureId ? getters.getChartIdFromFigureId(figureId) : initialProps.chartId;
        if (!chartId || !getters.isChartDefined(chartId)) {
            return { isOpen: false };
        }
        return { isOpen: true, props: { chartId } };
    },
});
sidePanelRegistry.add("FindAndReplace", {
    title: _t("Find and Replace"),
    Body: FindAndReplacePanel,
});
sidePanelRegistry.add("CustomCurrency", {
    title: _t("Custom currency format"),
    Body: CustomCurrencyPanel,
});
sidePanelRegistry.add("SplitToColumns", {
    title: _t("Split text into columns"),
    Body: SplitIntoColumnsPanel,
});
sidePanelRegistry.add("Settings", {
    title: _t("Spreadsheet settings"),
    Body: SettingsPanel,
});
sidePanelRegistry.add("RemoveDuplicates", {
    title: _t("Remove duplicates"),
    Body: RemoveDuplicatesPanel,
});
sidePanelRegistry.add("DataValidation", {
    title: _t("Data validation"),
    Body: DataValidationPanel,
});
sidePanelRegistry.add("DataValidationEditor", {
    title: _t("Data validation"),
    Body: DataValidationEditor,
});
sidePanelRegistry.add("MoreFormats", {
    title: _t("More date formats"),
    Body: MoreFormatsPanel,
});
sidePanelRegistry.add("TableSidePanel", {
    title: _t("Edit table"),
    Body: TablePanel,
    computeState: (getters) => {
        const table = getters.getFirstTableInSelection();
        if (!table) {
            return { isOpen: false };
        }
        const coreTable = getters.getCoreTable(getTableTopLeft(table));
        return { isOpen: true, props: { table: coreTable }, key: table.id };
    },
});
sidePanelRegistry.add("TableStyleEditorPanel", {
    title: _t("Create custom table style"),
    Body: TableStyleEditorPanel,
    computeState: (getters, initialProps) => {
        return {
            isOpen: true,
            props: { ...initialProps },
            key: initialProps.styleId ?? "new",
        };
    },
});
sidePanelRegistry.add("PivotSidePanel", {
    title: (env, props) => {
        return _t("Pivot #%s", env.model.getters.getPivotFormulaId(props.pivotId));
    },
    Body: PivotSidePanel,
    computeState: (getters, props) => {
        return {
            isOpen: getters.isExistingPivot(props.pivotId),
            props,
            key: `pivot_key_${props.pivotId}`,
        };
    },
});
sidePanelRegistry.add("PivotMeasureDisplayPanel", {
    title: (env, props) => {
        const measure = env.model.getters.getPivot(props.pivotId).getMeasure(props.measure.id);
        return _t('Measure "%s" options', measure.displayName);
    },
    Body: PivotMeasureDisplayPanel,
    computeState: (getters, props) => {
        try {
            // This will throw if the pivot or measure does not exist
            getters.getPivot(props.pivotId).getMeasure(props.measure.id);
            return {
                isOpen: true,
                props,
                key: `pivot_measure_display_${props.pivotId}_${props.measure.id}`,
            };
        }
        catch (e) {
            return { isOpen: false };
        }
    },
});
sidePanelRegistry.add("CarouselPanel", {
    title: _t("Carousel"),
    Body: CarouselPanel,
    computeState: (getters, initialProps) => {
        const figureId = initialProps.figureId || getters.getSelectedFigureId();
        if (!figureId || !getters.doesCarouselExist(figureId)) {
            return { isOpen: false };
        }
        return { isOpen: true, props: { figureId } };
    },
});

class ScreenWidthStore {
    mutators = ["setSmallThreshhold"];
    _isSmallCallback = () => false;
    get isSmall() {
        return this._isSmallCallback();
    }
    setSmallThreshhold(isSmall) {
        this._isSmallCallback = isSmall;
    }
}

const DEFAULT_SIDE_PANEL_SIZE = 350;
const COLLAPSED_SIDE_PANEL_SIZE = 45;
const MIN_SHEET_VIEW_WIDTH = 150;
class SidePanelStore extends SpreadsheetStore {
    mutators = [
        "open",
        "replace",
        "toggle",
        "close",
        "changePanelSize",
        "resetPanelSize",
        "togglePinPanel",
        "closeMainPanel",
        "changeSpreadsheetWidth",
        "toggleCollapsePanel",
    ];
    mainPanel = undefined;
    secondaryPanel;
    availableWidth = 0;
    screenWidthStore = this.get(ScreenWidthStore);
    get isMainPanelOpen() {
        return this.mainPanel && this.mainPanel.componentTag
            ? this.computeState(this.mainPanel).isOpen
            : false;
    }
    get isSecondaryPanelOpen() {
        return this.secondaryPanel && this.secondaryPanel.componentTag
            ? this.computeState(this.secondaryPanel).isOpen
            : false;
    }
    get mainPanelProps() {
        return this.mainPanel ? this.getPanelProps(this.mainPanel) : undefined;
    }
    get mainPanelKey() {
        return this.mainPanel ? this.getPanelKey(this.mainPanel) : undefined;
    }
    get secondaryPanelProps() {
        return this.secondaryPanel ? this.getPanelProps(this.secondaryPanel) : undefined;
    }
    get secondaryPanelKey() {
        return this.secondaryPanel ? this.getPanelKey(this.secondaryPanel) : undefined;
    }
    get totalPanelSize() {
        return (this.mainPanel?.size || 0) + (this.secondaryPanel?.size ?? 0);
    }
    getPanelProps(panelInfo) {
        const state = this.computeState(panelInfo);
        if (state.isOpen) {
            panelInfo.currentPanelProps = state.props ?? panelInfo.currentPanelProps;
            return state.props ?? {};
        }
        return {};
    }
    getPanelKey(panelInfo) {
        const state = this.computeState(panelInfo);
        if (state.isOpen) {
            return state.key;
        }
        return undefined;
    }
    open(componentTag, currentPanelProps = {}) {
        if (this.screenWidthStore.isSmall) {
            return;
        }
        const newPanelInfo = { currentPanelProps, componentTag, size: DEFAULT_SIDE_PANEL_SIZE };
        const state = this.computeState(newPanelInfo);
        if (!state.isOpen) {
            return;
        }
        if (!this.mainPanel || !this.mainPanel.isPinned || this.mainPanelKey === state.key) {
            this._openPanel("mainPanel", newPanelInfo, state);
            return;
        }
        // Try to open secondary panel if main panel is pinned
        const nonCollapsedPanelSize = this.mainPanel.isCollapsed
            ? DEFAULT_SIDE_PANEL_SIZE
            : this.mainPanel.size;
        if (!this.secondaryPanel &&
            nonCollapsedPanelSize + DEFAULT_SIDE_PANEL_SIZE > this.availableWidth) {
            this.get(NotificationStore).notifyUser({
                sticky: false,
                type: "warning",
                text: _t("The window is too small to display multiple side panels."),
            });
            return;
        }
        this._openPanel("secondaryPanel", newPanelInfo, state);
    }
    replace(componentTag, currentPanelKey, currentPanelProps = {}) {
        const newPanelInfo = { currentPanelProps, componentTag, size: DEFAULT_SIDE_PANEL_SIZE };
        const state = this.computeState(newPanelInfo);
        if (!state.isOpen) {
            return;
        }
        const ensureMainPanelExpanded = () => {
            if (this.mainPanel?.isCollapsed) {
                this.toggleCollapsePanel("mainPanel");
            }
        };
        // Close the current panel if the target panel is already open
        const isMainPanel = this.mainPanelKey === state.key;
        const isSecondaryPanel = this.secondaryPanelKey === state.key;
        if (isMainPanel && this.secondaryPanel) {
            this.close();
            ensureMainPanelExpanded();
            return;
        }
        if (isSecondaryPanel) {
            this.closeMainPanel();
            this.togglePinPanel();
            ensureMainPanelExpanded();
            return;
        }
        const targetPanel = this.mainPanelKey === currentPanelKey ? "mainPanel" : "secondaryPanel";
        this._openPanel(targetPanel, newPanelInfo, state);
    }
    _openPanel(panel, newPanel, state) {
        const currentPanel = this[panel];
        if (currentPanel && newPanel.componentTag !== currentPanel.componentTag) {
            currentPanel.currentPanelProps?.onCloseSidePanel?.();
        }
        this[panel] = {
            currentPanelProps: state.props ?? {},
            componentTag: newPanel.componentTag,
            size: currentPanel?.size || DEFAULT_SIDE_PANEL_SIZE,
            isCollapsed: currentPanel?.isCollapsed || false,
            isPinned: currentPanel && "isPinned" in currentPanel ? currentPanel.isPinned : false,
        };
        if (this[panel].isCollapsed) {
            this.toggleCollapsePanel(panel);
        }
    }
    toggle(componentTag, panelProps) {
        const panel = this.mainPanel?.isPinned ? this.secondaryPanel : this.mainPanel;
        if (panel && componentTag === panel.componentTag) {
            this.close();
        }
        else {
            this.open(componentTag, panelProps);
        }
    }
    close() {
        if (this.mainPanel?.isPinned) {
            if (this.secondaryPanel) {
                this.secondaryPanel.currentPanelProps.onCloseSidePanel?.();
                this.secondaryPanel = undefined;
            }
            return;
        }
        this.mainPanel?.currentPanelProps.onCloseSidePanel?.();
        this.mainPanel = undefined;
    }
    closeMainPanel() {
        this.mainPanel?.currentPanelProps.onCloseSidePanel?.();
        this.mainPanel = this.secondaryPanel || undefined;
        this.secondaryPanel = undefined;
    }
    changePanelSize(panel, size) {
        const panelInfo = this[panel];
        if (!panelInfo || ("isCollapsed" in panelInfo && panelInfo.isCollapsed)) {
            return;
        }
        size = Math.max(size, DEFAULT_SIDE_PANEL_SIZE);
        let otherPanelSize = panel === "mainPanel" ? this.secondaryPanel?.size || 0 : this.mainPanel?.size || 0;
        if (size > this.availableWidth - otherPanelSize) {
            if (panel === "mainPanel" && this.secondaryPanel) {
                // reduce the secondary panel size to fit the main panel
                this.secondaryPanel.size = Math.max(this.availableWidth - size, DEFAULT_SIDE_PANEL_SIZE);
                otherPanelSize = this.secondaryPanel.size;
            }
            size = Math.max(this.availableWidth - otherPanelSize, DEFAULT_SIDE_PANEL_SIZE);
        }
        panelInfo.size = size;
    }
    resetPanelSize(panel) {
        if (this[panel]) {
            this[panel].size = DEFAULT_SIDE_PANEL_SIZE;
        }
    }
    togglePinPanel() {
        if (!this.mainPanel) {
            return;
        }
        this.mainPanel.isPinned = !this.mainPanel.isPinned;
        if (!this.mainPanel.isPinned && this.secondaryPanel) {
            this.secondaryPanel?.currentPanelProps.onCloseSidePanel?.();
            this.mainPanel = this.secondaryPanel;
            this.secondaryPanel = undefined;
        }
    }
    toggleCollapsePanel(panel) {
        const panelInfo = this[panel];
        if (!panelInfo) {
            return;
        }
        if (panelInfo.isCollapsed) {
            panelInfo.isCollapsed = false;
            this.changePanelSize(panel, DEFAULT_SIDE_PANEL_SIZE);
        }
        else {
            panelInfo.isCollapsed = true;
            panelInfo.size = COLLAPSED_SIDE_PANEL_SIZE;
        }
    }
    computeState({ componentTag, currentPanelProps: initialPanelProps, }) {
        const customComputeState = sidePanelRegistry.get(componentTag).computeState;
        const state = customComputeState
            ? customComputeState(this.getters, initialPanelProps)
            : { isOpen: true, props: initialPanelProps };
        return state.isOpen ? { ...state, key: state.key || componentTag } : state;
    }
    changeSpreadsheetWidth(width) {
        this.availableWidth = width - MIN_SHEET_VIEW_WIDTH;
        if (this.secondaryPanel && width - this.totalPanelSize < MIN_SHEET_VIEW_WIDTH) {
            this.secondaryPanel?.currentPanelProps.onCloseSidePanel?.();
            this.secondaryPanel = undefined;
        }
        if (this.mainPanel && width - this.totalPanelSize < MIN_SHEET_VIEW_WIDTH) {
            this.mainPanel.size = Math.max(width - MIN_SHEET_VIEW_WIDTH, DEFAULT_SIDE_PANEL_SIZE);
        }
    }
}

const SIZE = 3;
const COLOR = "#777";
css /* scss */ `
  .o-table-resizer {
    width: ${SIZE * 2}px;
    height: ${SIZE * 2}px;
    border-bottom: ${SIZE}px solid ${COLOR};
    border-right: ${SIZE}px solid ${COLOR};
    cursor: nwse-resize;
  }
`;
class TableResizer extends Component {
    static template = "o-spreadsheet-TableResizer";
    static props = { table: Object };
    state = useState({ highlightZone: undefined });
    dragNDropGrid = useDragAndDropBeyondTheViewport(this.env);
    setup() {
        useHighlights(this);
    }
    get containerStyle() {
        const tableZone = this.props.table.range.zone;
        const bottomRight = { ...tableZone, left: tableZone.right, top: tableZone.bottom };
        const rect = this.env.model.getters.getVisibleRect(bottomRight);
        if (rect.height === 0 || rect.width === 0) {
            return cssPropertiesToCss({ display: "none" });
        }
        return cssPropertiesToCss({
            top: `${rect.y + rect.height - SIZE * 2}px`,
            left: `${rect.x + rect.width - SIZE * 2}px`,
        });
    }
    onMouseDown(ev) {
        const tableZone = this.props.table.range.zone;
        const topLeft = { col: tableZone.left, row: tableZone.top };
        document.body.style.cursor = "nwse-resize";
        const onMouseUp = () => {
            document.body.style.cursor = "";
            const newTableZone = this.state.highlightZone;
            if (!newTableZone)
                return;
            const sheetId = this.props.table.range.sheetId;
            this.env.model.dispatch("RESIZE_TABLE", {
                sheetId,
                zone: this.props.table.range.zone,
                newTableRange: this.env.model.getters.getRangeDataFromZone(sheetId, newTableZone),
            });
            this.state.highlightZone = undefined;
        };
        const onMouseMove = (col, row, ev) => {
            this.state.highlightZone = {
                left: topLeft.col,
                top: topLeft.row,
                right: Math.max(col, topLeft.col),
                bottom: Math.max(row, topLeft.row),
            };
        };
        this.dragNDropGrid.start(ev, onMouseMove, onMouseUp);
    }
    get highlights() {
        if (!this.state.highlightZone)
            return [];
        return [
            {
                range: this.env.model.getters.getRangeFromZone(this.props.table.range.sheetId, this.state.highlightZone),
                color: COLOR,
                noFill: true,
            },
        ];
    }
}

const registries$1 = {
    ROW: rowMenuRegistry,
    COL: colMenuRegistry,
    CELL: cellMenuRegistry,
    GROUP_HEADERS: groupHeadersMenuRegistry,
    UNGROUP_HEADERS: unGroupHeadersMenuRegistry,
};
// -----------------------------------------------------------------------------
// JS
// -----------------------------------------------------------------------------
class Grid extends Component {
    static template = "o-spreadsheet-Grid";
    static props = {
        exposeFocus: Function,
        getGridSize: Function,
    };
    static components = {
        GridComposer,
        GridOverlay,
        GridPopover,
        HeadersOverlay,
        MenuPopover,
        Autofill,
        ClientTag,
        Highlight,
        Popover,
        VerticalScrollBar,
        HorizontalScrollBar,
        TableResizer,
        Selection,
    };
    HEADER_HEIGHT = HEADER_HEIGHT;
    HEADER_WIDTH = HEADER_WIDTH;
    menuState;
    gridRef;
    highlightStore;
    cellPopovers;
    composerFocusStore;
    DOMFocusableElementStore;
    paintFormatStore;
    clientFocusStore;
    dragNDropGrid = useDragAndDropBeyondTheViewport(this.env);
    onMouseWheel;
    hoveredCell;
    sidePanel;
    setup() {
        this.highlightStore = useStore(HighlightStore);
        this.menuState = useState({
            isOpen: false,
            anchorRect: null,
            menuItems: [],
        });
        this.gridRef = useRef("grid");
        this.hoveredCell = useStore(DelayedHoveredCellStore);
        this.composerFocusStore = useStore(ComposerFocusStore);
        this.DOMFocusableElementStore = useStore(DOMFocusableElementStore);
        this.sidePanel = useStore(SidePanelStore);
        this.paintFormatStore = useStore(PaintFormatStore);
        this.clientFocusStore = useStore(ClientFocusStore);
        useStore(ArrayFormulaHighlight);
        useChildSubEnv({ getPopoverContainerRect: () => this.getGridRect() });
        useExternalListener(document.body, "cut", this.copy.bind(this, true));
        useExternalListener(document.body, "copy", this.copy.bind(this, false));
        useExternalListener(document.body, "paste", this.paste);
        onMounted(() => this.focusDefaultElement());
        this.props.exposeFocus(() => this.focusDefaultElement());
        useGridDrawing("canvas", this.env.model, () => this.env.model.getters.getSheetViewDimensionWithHeaders());
        this.onMouseWheel = useWheelHandler((deltaX, deltaY) => {
            this.moveCanvas(deltaX, deltaY);
            this.hoveredCell.clear();
        });
        this.cellPopovers = useStore(CellPopoverStore);
        useEffect((isMainPanelOpen, isSecondaryPanelOpen) => {
            if (!isMainPanelOpen && !isSecondaryPanelOpen) {
                this.DOMFocusableElementStore.focus();
            }
        }, () => [this.sidePanel.isMainPanelOpen, this.sidePanel.isSecondaryPanelOpen]);
        useTouchScroll(this.gridRef, this.moveCanvas.bind(this), () => {
            const { scrollY } = this.env.model.getters.getActiveSheetScrollInfo();
            return scrollY > 0;
        }, () => {
            const { maxOffsetY } = this.env.model.getters.getMaximumSheetOffset();
            const { scrollY } = this.env.model.getters.getActiveSheetScrollInfo();
            return scrollY < maxOffsetY;
        });
    }
    get highlights() {
        return this.highlightStore.highlights;
    }
    get gridOverlayDimensions() {
        return cssPropertiesToCss({
            top: `${HEADER_HEIGHT}px`,
            left: `${HEADER_WIDTH}px`,
            height: `calc(100% - ${HEADER_HEIGHT + SCROLLBAR_WIDTH}px)`,
            width: `calc(100% - ${HEADER_WIDTH + SCROLLBAR_WIDTH}px)`,
        });
    }
    onClosePopover() {
        if (this.cellPopovers.isOpen) {
            this.cellPopovers.close();
        }
        this.focusDefaultElement();
    }
    // this map will handle most of the actions that should happen on key down. The arrow keys are managed in the key
    // down itself
    keyDownMapping = {
        Enter: () => {
            const cell = this.env.model.getters.getActiveCell();
            cell.type === CellValueType.empty
                ? this.onComposerCellFocused()
                : this.onComposerContentFocused();
        },
        Tab: () => this.env.model.selection.moveAnchorCell("right", 1),
        "Shift+Tab": () => this.env.model.selection.moveAnchorCell("left", 1),
        F2: () => {
            const cell = this.env.model.getters.getActiveCell();
            cell.type === CellValueType.empty
                ? this.onComposerCellFocused()
                : this.onComposerContentFocused();
        },
        Delete: () => {
            this.env.model.dispatch("DELETE_UNFILTERED_CONTENT", {
                sheetId: this.env.model.getters.getActiveSheetId(),
                target: this.env.model.getters.getSelectedZones(),
            });
        },
        Backspace: () => {
            this.env.model.dispatch("DELETE_UNFILTERED_CONTENT", {
                sheetId: this.env.model.getters.getActiveSheetId(),
                target: this.env.model.getters.getSelectedZones(),
            });
        },
        Escape: () => {
            /** TODO: Clean once we introduce proper focus on sub components. Grid should not have to handle all this logic */
            if (this.cellPopovers.isOpen) {
                this.cellPopovers.close();
            }
            else if (this.menuState.isOpen) {
                this.closeMenu();
            }
            else if (this.paintFormatStore.isActive) {
                this.paintFormatStore.cancel();
            }
            else {
                this.env.model.dispatch("CLEAN_CLIPBOARD_HIGHLIGHT");
            }
        },
        "Ctrl+A": () => this.env.model.selection.loopSelection(),
        "Ctrl+Z": () => this.env.model.dispatch("REQUEST_UNDO"),
        "Ctrl+Y": () => this.env.model.dispatch("REQUEST_REDO"),
        F4: () => this.env.model.dispatch("REQUEST_REDO"),
        "Ctrl+B": () => this.env.model.dispatch("SET_FORMATTING", {
            sheetId: this.env.model.getters.getActiveSheetId(),
            target: this.env.model.getters.getSelectedZones(),
            style: { bold: !this.env.model.getters.getCurrentStyle().bold },
        }),
        "Ctrl+I": () => this.env.model.dispatch("SET_FORMATTING", {
            sheetId: this.env.model.getters.getActiveSheetId(),
            target: this.env.model.getters.getSelectedZones(),
            style: { italic: !this.env.model.getters.getCurrentStyle().italic },
        }),
        "Ctrl+U": () => this.env.model.dispatch("SET_FORMATTING", {
            sheetId: this.env.model.getters.getActiveSheetId(),
            target: this.env.model.getters.getSelectedZones(),
            style: { underline: !this.env.model.getters.getCurrentStyle().underline },
        }),
        "Ctrl+O": () => CREATE_IMAGE(this.env),
        "Alt+=": () => {
            const sheetId = this.env.model.getters.getActiveSheetId();
            const mainSelectedZone = this.env.model.getters.getSelectedZone();
            const { anchor } = this.env.model.getters.getSelection();
            const sums = this.env.model.getters.getAutomaticSums(sheetId, mainSelectedZone, anchor.cell);
            if (this.env.model.getters.isSingleCellOrMerge(sheetId, mainSelectedZone) ||
                (this.env.model.getters.isEmpty(sheetId, mainSelectedZone) && sums.length <= 1)) {
                const zone = sums[0]?.zone;
                const zoneXc = zone ? this.env.model.getters.zoneToXC(sheetId, sums[0].zone) : "";
                const formula = `=SUM(${zoneXc})`;
                this.onComposerCellFocused(formula, { start: 5, end: 5 + zoneXc.length });
            }
            else {
                this.env.model.dispatch("SUM_SELECTION");
            }
        },
        "Alt+Enter": () => {
            const cell = this.env.model.getters.getActiveCell();
            if (cell.link) {
                openLink(cell.link, this.env);
            }
        },
        "Ctrl+Home": () => {
            const sheetId = this.env.model.getters.getActiveSheetId();
            const { col, row } = this.env.model.getters.getNextVisibleCellPosition({
                sheetId,
                col: 0,
                row: 0,
            });
            this.env.model.selection.selectCell(col, row);
        },
        "Ctrl+End": () => {
            const sheetId = this.env.model.getters.getActiveSheetId();
            const col = this.env.model.getters.findVisibleHeader(sheetId, "COL", this.env.model.getters.getNumberCols(sheetId) - 1, 0);
            const row = this.env.model.getters.findVisibleHeader(sheetId, "ROW", this.env.model.getters.getNumberRows(sheetId) - 1, 0);
            this.env.model.selection.selectCell(col, row);
        },
        "Shift+ ": () => {
            const sheetId = this.env.model.getters.getActiveSheetId();
            const newZone = {
                ...this.env.model.getters.getSelectedZone(),
                left: 0,
                right: this.env.model.getters.getNumberCols(sheetId) - 1,
            };
            const position = this.env.model.getters.getActivePosition();
            this.env.model.selection.selectZone({ cell: position, zone: newZone });
        },
        "Ctrl+ ": () => {
            const sheetId = this.env.model.getters.getActiveSheetId();
            const newZone = {
                ...this.env.model.getters.getSelectedZone(),
                top: 0,
                bottom: this.env.model.getters.getNumberRows(sheetId) - 1,
            };
            const position = this.env.model.getters.getActivePosition();
            this.env.model.selection.selectZone({ cell: position, zone: newZone });
        },
        "Ctrl+D": async () => this.env.model.dispatch("COPY_PASTE_CELLS_ABOVE"),
        "Ctrl+R": async () => this.env.model.dispatch("COPY_PASTE_CELLS_ON_LEFT"),
        "Ctrl+H": () => this.sidePanel.open("FindAndReplace", {}),
        "Ctrl+F": () => this.sidePanel.open("FindAndReplace", {}),
        "Ctrl+Shift+E": () => this.setHorizontalAlign("center"),
        "Ctrl+Shift+L": () => this.setHorizontalAlign("left"),
        "Ctrl+Shift+R": () => this.setHorizontalAlign("right"),
        "Ctrl+Shift+V": () => PASTE_AS_VALUE_ACTION(this.env),
        "Ctrl+Shift+<": () => this.clearFormatting(), // for qwerty
        "Ctrl+<": () => this.clearFormatting(), // for azerty
        "Ctrl+Shift+ ": () => {
            this.env.model.selection.selectAll();
        },
        "Ctrl+Alt+=": () => {
            const activeCols = this.env.model.getters.getActiveCols();
            const activeRows = this.env.model.getters.getActiveRows();
            const isSingleSelection = this.env.model.getters.getSelectedZones().length === 1;
            const areFullCols = activeCols.size > 0 && isSingleSelection;
            const areFullRows = activeRows.size > 0 && isSingleSelection;
            if (areFullCols && !areFullRows) {
                INSERT_COLUMNS_BEFORE_ACTION(this.env);
            }
            else if (areFullRows && !areFullCols) {
                INSERT_ROWS_BEFORE_ACTION(this.env);
            }
        },
        "Ctrl+Alt+-": () => {
            const columns = [...this.env.model.getters.getActiveCols()];
            const rows = [...this.env.model.getters.getActiveRows()];
            if (columns.length > 0 && rows.length === 0) {
                this.env.model.dispatch("REMOVE_COLUMNS_ROWS", {
                    sheetId: this.env.model.getters.getActiveSheetId(),
                    sheetName: this.env.model.getters.getActiveSheetName(),
                    dimension: "COL",
                    elements: columns,
                });
            }
            else if (rows.length > 0 && columns.length === 0) {
                this.env.model.dispatch("REMOVE_COLUMNS_ROWS", {
                    sheetId: this.env.model.getters.getActiveSheetId(),
                    sheetName: this.env.model.getters.getActiveSheetName(),
                    dimension: "ROW",
                    elements: rows,
                });
            }
        },
        "Shift+PageDown": () => {
            this.env.model.dispatch("ACTIVATE_NEXT_SHEET");
        },
        "Shift+PageUp": () => {
            this.env.model.dispatch("ACTIVATE_PREVIOUS_SHEET");
        },
        PageDown: () => this.env.model.dispatch("SHIFT_VIEWPORT_DOWN"),
        PageUp: () => this.env.model.dispatch("SHIFT_VIEWPORT_UP"),
        "Ctrl+K": () => INSERT_LINK(this.env),
        "Alt+Shift+ArrowRight": () => this.processHeaderGroupingKey("right"),
        "Alt+Shift+ArrowLeft": () => this.processHeaderGroupingKey("left"),
        "Alt+Shift+ArrowUp": () => this.processHeaderGroupingKey("up"),
        "Alt+Shift+ArrowDown": () => this.processHeaderGroupingKey("down"),
    };
    focusDefaultElement() {
        if (!this.env.model.getters.getSelectedFigureId() &&
            this.composerFocusStore.activeComposer.editionMode === "inactive") {
            this.DOMFocusableElementStore.focus();
        }
    }
    get gridEl() {
        if (!this.gridRef.el) {
            throw new Error("Grid el is not defined.");
        }
        return this.gridRef.el;
    }
    getAutofillPosition() {
        const zone = this.env.model.getters.getSelectedZone();
        const rect = this.env.model.getters.getVisibleRect(zone);
        return {
            left: rect.x + rect.width - AUTOFILL_EDGE_LENGTH / 2,
            top: rect.y + rect.height - AUTOFILL_EDGE_LENGTH / 2,
        };
    }
    get isAutofillVisible() {
        const zone = this.env.model.getters.getSelectedZone();
        const rect = this.env.model.getters.getVisibleRect({
            left: zone.right,
            right: zone.right,
            top: zone.bottom,
            bottom: zone.bottom,
        });
        return !(rect.width === 0 || rect.height === 0);
    }
    onGridResized() {
        const { height, width } = this.props.getGridSize();
        this.env.model.dispatch("RESIZE_SHEETVIEW", {
            width: width - HEADER_WIDTH,
            height: height - HEADER_HEIGHT,
            gridOffsetX: HEADER_WIDTH,
            gridOffsetY: HEADER_HEIGHT,
        });
    }
    moveCanvas(deltaX, deltaY) {
        const { scrollX, scrollY } = this.env.model.getters.getActiveSheetScrollInfo();
        this.env.model.dispatch("SET_VIEWPORT_OFFSET", {
            offsetX: scrollX + deltaX,
            offsetY: scrollY + deltaY,
        });
    }
    processSpaceKey(ev) {
        if (this.env.model.getters.hasBooleanValidationInZones(this.env.model.getters.getSelectedZones())) {
            ev.preventDefault();
            ev.stopPropagation();
            this.env.model.dispatch("TOGGLE_CHECKBOX", {
                sheetId: this.env.model.getters.getActiveSheetId(),
                target: this.env.model.getters.getSelectedZones(),
            });
        }
    }
    getClientPositionKey(client) {
        return `${client.id}-${client.position?.sheetId}-${client.position?.col}-${client.position?.row}`;
    }
    isCellHovered(col, row) {
        return this.hoveredCell.col === col && this.hoveredCell.row === row;
    }
    get focusedClients() {
        return this.clientFocusStore.focusedClients;
    }
    getGridRect() {
        return {
            ...getRefBoundingRect(this.gridRef),
            ...this.env.model.getters.getSheetViewDimensionWithHeaders(),
        };
    }
    // ---------------------------------------------------------------------------
    // Zone selection with mouse
    // ---------------------------------------------------------------------------
    onCellClicked(col, row, modifiers, ev) {
        ev.preventDefault();
        if (this.composerFocusStore.activeComposer.editionMode === "editing") {
            this.composerFocusStore.activeComposer.stopEdition();
        }
        if (modifiers.expandZone) {
            this.env.model.selection.setAnchorCorner(col, row);
        }
        else if (modifiers.addZone) {
            this.env.model.selection.addCellToSelection(col, row);
        }
        else {
            this.env.model.selection.selectCell(col, row);
        }
        if (this.env.isMobile()) {
            return;
        }
        let prevCol = col;
        let prevRow = row;
        const onMouseMove = (col, row, ev) => {
            // When selecting cells during the edition, we don't want to avoid the default
            // browser behaviour that will select the text inside the composer
            // (see related commit msg for more information)
            ev.preventDefault();
            if ((col !== prevCol && col !== -1) || (row !== prevRow && row !== -1)) {
                prevCol = col === -1 ? prevCol : col;
                prevRow = row === -1 ? prevRow : row;
                this.env.model.selection.setAnchorCorner(prevCol, prevRow);
            }
        };
        const onMouseUp = () => {
            if (this.paintFormatStore.isActive) {
                this.paintFormatStore.pasteFormat(this.env.model.getters.getSelectedZones());
            }
        };
        this.dragNDropGrid.start(ev, onMouseMove, onMouseUp);
    }
    onCellDoubleClicked(col, row) {
        const sheetId = this.env.model.getters.getActiveSheetId();
        ({ col, row } = this.env.model.getters.getMainCellPosition({ sheetId, col, row }));
        const cell = this.env.model.getters.getEvaluatedCell({ sheetId, col, row });
        if (cell.type === CellValueType.empty) {
            this.onComposerCellFocused();
        }
        else {
            this.onComposerContentFocused();
        }
    }
    // ---------------------------------------------------------------------------
    // Keyboard interactions
    // ---------------------------------------------------------------------------
    processArrows(ev) {
        ev.preventDefault();
        ev.stopPropagation();
        if (this.cellPopovers.isOpen) {
            this.cellPopovers.close();
        }
        updateSelectionWithArrowKeys(ev, this.env.model.selection);
        if (this.paintFormatStore.isActive) {
            this.paintFormatStore.pasteFormat(this.env.model.getters.getSelectedZones());
        }
    }
    onKeydown(ev) {
        const keyDownString = keyboardEventToShortcutString(ev);
        const handler = this.keyDownMapping[keyDownString];
        if (handler) {
            ev.preventDefault();
            ev.stopPropagation();
            handler();
            return;
        }
        // Space key is handled separately because the default and the propagation
        // of the event should be stopped conditionally (presence of a validation rule)
        if (keyDownString === " ") {
            this.processSpaceKey(ev);
            return;
        }
        if (ev.key.startsWith("Arrow")) {
            this.processArrows(ev);
            return;
        }
    }
    // ---------------------------------------------------------------------------
    // Context Menu
    // ---------------------------------------------------------------------------
    onInputContextMenu(ev) {
        ev.preventDefault();
        const lastZone = this.env.model.getters.getSelectedZone();
        const { left: col, top: row } = lastZone;
        let type = "CELL";
        this.composerFocusStore.activeComposer.stopEdition();
        if (this.env.model.getters.getActiveCols().has(col)) {
            type = "COL";
        }
        else if (this.env.model.getters.getActiveRows().has(row)) {
            type = "ROW";
        }
        const { x, y, width } = this.env.model.getters.getVisibleRect(lastZone);
        const gridRect = this.getGridRect();
        this.toggleContextMenu(type, gridRect.x + x + width, gridRect.y + y);
    }
    onCellRightClicked(col, row, { x, y }) {
        const zones = this.env.model.getters.getSelectedZones();
        const lastZone = zones[zones.length - 1];
        let type = "CELL";
        if (!isInside(col, row, lastZone)) {
            this.env.model.selection.getBackToDefault();
            this.env.model.selection.selectCell(col, row);
        }
        else {
            if (this.env.model.getters.getActiveCols().has(col)) {
                type = "COL";
            }
            else if (this.env.model.getters.getActiveRows().has(row)) {
                type = "ROW";
            }
        }
        this.toggleContextMenu(type, x, y);
    }
    toggleContextMenu(type, x, y) {
        if (this.cellPopovers.isOpen) {
            this.cellPopovers.close();
        }
        this.menuState.isOpen = true;
        this.menuState.anchorRect = { x, y, width: 0, height: 0 };
        this.menuState.menuItems = registries$1[type].getMenuItems();
    }
    async copy(cut, ev) {
        if (!this.gridEl.contains(document.activeElement)) {
            return;
        }
        /* If we are currently editing a cell, let the default behavior */
        if (this.composerFocusStore.activeComposer.editionMode !== "inactive") {
            return;
        }
        if (cut) {
            interactiveCut(this.env);
        }
        else {
            this.env.model.dispatch("COPY");
        }
        const osContent = await this.env.model.getters.getClipboardTextAndImageContent();
        await this.env.clipboard.write(osContent);
        ev.preventDefault();
    }
    async paste(ev) {
        if (!this.gridEl.contains(document.activeElement)) {
            return;
        }
        ev.preventDefault();
        const clipboardData = ev.clipboardData;
        if (!clipboardData) {
            return;
        }
        const image = [...clipboardData.files]?.find((file) => AllowedImageMimeTypes.includes(file.type));
        const osClipboard = {
            content: {
                [ClipboardMIMEType.PlainText]: clipboardData?.getData(ClipboardMIMEType.PlainText),
                [ClipboardMIMEType.Html]: clipboardData?.getData(ClipboardMIMEType.Html),
            },
        };
        if (image) {
            // TODO: support import of multiple images
            osClipboard.content[image.type] = image;
        }
        const target = this.env.model.getters.getSelectedZones();
        const isCutOperation = this.env.model.getters.isCutOperation();
        const clipboardId = this.env.model.getters.getClipboardId();
        const osClipboardContent = parseOSClipboardContent(osClipboard.content);
        const osClipboardId = osClipboardContent.data?.clipboardId;
        if (clipboardId === osClipboardId) {
            interactivePaste(this.env, target);
        }
        else {
            await interactivePasteFromOS(this.env, target, osClipboardContent);
        }
        if (isCutOperation) {
            await this.env.clipboard.write({ [ClipboardMIMEType.PlainText]: "" });
        }
    }
    clearFormatting() {
        this.env.model.dispatch("CLEAR_FORMATTING", {
            sheetId: this.env.model.getters.getActiveSheetId(),
            target: this.env.model.getters.getSelectedZones(),
        });
    }
    setHorizontalAlign(align) {
        this.env.model.dispatch("SET_FORMATTING", {
            sheetId: this.env.model.getters.getActiveSheetId(),
            target: this.env.model.getters.getSelectedZones(),
            style: { align },
        });
    }
    closeMenu() {
        this.menuState.isOpen = false;
        this.focusDefaultElement();
    }
    processHeaderGroupingKey(direction) {
        if (this.env.model.getters.getSelectedZones().length !== 1) {
            return;
        }
        const selectingRows = this.env.model.getters.getActiveRows().size > 0;
        const selectingCols = this.env.model.getters.getActiveCols().size > 0;
        if (selectingCols && selectingRows) {
            this.processHeaderGroupingEventOnWholeSheet(direction);
        }
        else if (selectingCols) {
            this.processHeaderGroupingEventOnHeaders(direction, "COL");
        }
        else if (selectingRows) {
            this.processHeaderGroupingEventOnHeaders(direction, "ROW");
        }
        else {
            this.processHeaderGroupingEventOnGrid(direction);
        }
    }
    processHeaderGroupingEventOnHeaders(direction, dimension) {
        const sheetId = this.env.model.getters.getActiveSheetId();
        const zone = this.env.model.getters.getSelectedZone();
        const start = dimension === "COL" ? zone.left : zone.top;
        const end = dimension === "COL" ? zone.right : zone.bottom;
        switch (direction) {
            case "right":
                this.env.model.dispatch("GROUP_HEADERS", { sheetId, dimension: dimension, start, end });
                break;
            case "left":
                this.env.model.dispatch("UNGROUP_HEADERS", { sheetId, dimension: dimension, start, end });
                break;
            case "down":
                this.env.model.dispatch("UNFOLD_HEADER_GROUPS_IN_ZONE", { sheetId, dimension, zone });
                break;
            case "up":
                this.env.model.dispatch("FOLD_HEADER_GROUPS_IN_ZONE", { sheetId, dimension, zone });
                break;
        }
    }
    processHeaderGroupingEventOnWholeSheet(direction) {
        const sheetId = this.env.model.getters.getActiveSheetId();
        if (direction === "up") {
            this.env.model.dispatch("FOLD_ALL_HEADER_GROUPS", { sheetId, dimension: "ROW" });
            this.env.model.dispatch("FOLD_ALL_HEADER_GROUPS", { sheetId, dimension: "COL" });
        }
        else if (direction === "down") {
            this.env.model.dispatch("UNFOLD_ALL_HEADER_GROUPS", { sheetId, dimension: "ROW" });
            this.env.model.dispatch("UNFOLD_ALL_HEADER_GROUPS", { sheetId, dimension: "COL" });
        }
    }
    processHeaderGroupingEventOnGrid(direction) {
        const sheetId = this.env.model.getters.getActiveSheetId();
        const zone = this.env.model.getters.getSelectedZone();
        switch (direction) {
            case "down":
                this.env.model.dispatch("UNFOLD_HEADER_GROUPS_IN_ZONE", {
                    sheetId,
                    dimension: "ROW",
                    zone: zone,
                });
                this.env.model.dispatch("UNFOLD_HEADER_GROUPS_IN_ZONE", {
                    sheetId,
                    dimension: "COL",
                    zone: zone,
                });
                break;
            case "up":
                this.env.model.dispatch("FOLD_HEADER_GROUPS_IN_ZONE", {
                    sheetId,
                    dimension: "ROW",
                    zone: zone,
                });
                this.env.model.dispatch("FOLD_HEADER_GROUPS_IN_ZONE", {
                    sheetId,
                    dimension: "COL",
                    zone: zone,
                });
                break;
            case "right": {
                const { x, y, width } = this.env.model.getters.getVisibleRect(zone);
                const gridRect = this.getGridRect();
                this.toggleContextMenu("GROUP_HEADERS", x + width + gridRect.x, y + gridRect.y);
                break;
            }
            case "left": {
                if (!canUngroupHeaders(this.env, "COL") && !canUngroupHeaders(this.env, "ROW")) {
                    return;
                }
                const { x, y, width } = this.env.model.getters.getVisibleRect(zone);
                const gridRect = this.getGridRect();
                this.toggleContextMenu("UNGROUP_HEADERS", x + width + gridRect.x, y + gridRect.y);
                break;
            }
        }
    }
    onComposerCellFocused(content, selection) {
        this.composerFocusStore.focusActiveComposer({ content, selection, focusMode: "cellFocus" });
    }
    onComposerContentFocused() {
        this.composerFocusStore.focusActiveComposer({ focusMode: "contentFocus" });
    }
    get staticTables() {
        const sheetId = this.env.model.getters.getActiveSheetId();
        return this.env.model.getters.getCoreTables(sheetId).filter(isStaticTable);
    }
    get displaySelectionHandler() {
        return this.env.isMobile() && this.composerFocusStore.activeComposer.editionMode === "inactive";
    }
}

/**
 * Registry to enable or disable the support of positional arguments
 * (with a leading #) in pivot functions
 * e.g. =PIVOT.VALUE(1,"probability","#stage",1)
 */
const supportedPivotPositionalFormulaRegistry = new Registry();
supportedPivotPositionalFormulaRegistry.add("SPREADSHEET", false);

class ClickableCellSortIcon extends Component {
    static template = "o-spreadsheet-ClickableCellSortIcon";
    static props = {
        position: Object,
        sortDirection: String,
    };
    hoveredTableStore;
    setup() {
        this.hoveredTableStore = useStore(HoveredTableStore);
    }
    get style() {
        const cellStyle = this.env.model.getters.getCellComputedStyle(this.props.position);
        return cssPropertiesToCss({
            color: cellStyle.textColor || TEXT_BODY_MUTED,
            "background-color": this.getBackgroundColor(cellStyle),
        });
    }
    get verticalJustifyClass() {
        const cellStyle = this.env.model.getters.getCellComputedStyle(this.props.position);
        switch (cellStyle.verticalAlign) {
            case "top":
                return "justify-content-start";
            case "middle":
                return "justify-content-center";
            case "bottom":
            default:
                return "justify-content-end";
        }
    }
    getBackgroundColor(cellStyle) {
        const overlayColor = this.hoveredTableStore.overlayColors.get(this.props.position);
        if (overlayColor) {
            return blendColors(cellStyle.fillColor || "#FFFFFF", overlayColor);
        }
        return cellStyle.fillColor || "#FFFFFF";
    }
}

class FullScreenFigure extends Component {
    static template = "o-spreadsheet-FullScreenFigure";
    static props = {};
    static components = { ChartFigure };
    fullScreenFigureStore;
    ref = useRef("fullScreenFigure");
    spreadsheetRect = useSpreadsheetRect();
    figureRegistry = figureRegistry;
    setup() {
        this.fullScreenFigureStore = useStore(FullScreenFigureStore);
        const animationStore = useStore(ChartAnimationStore);
        let lastFigureId = undefined;
        onWillUpdateProps(() => {
            if (lastFigureId !== this.figureUI?.id) {
                animationStore.enableAnimationForChart(this.chartId + "-fullscreen");
            }
            lastFigureId = this.figureUI?.id;
        });
        useEffect((el) => el?.focus(), () => [this.ref.el]);
    }
    get figureUI() {
        return this.fullScreenFigureStore.fullScreenFigure;
    }
    get chartId() {
        if (!this.figureUI)
            return undefined;
        return this.env.model.getters.getChartIdFromFigureId(this.figureUI?.id);
    }
    exitFullScreen() {
        if (this.figureUI) {
            this.fullScreenFigureStore.toggleFullScreenFigure(this.figureUI.id);
        }
    }
    onKeyDown(ev) {
        if (ev.key === "Escape") {
            this.exitFullScreen();
        }
    }
    get figureComponent() {
        if (!this.figureUI)
            return undefined;
        return figureRegistry.get(this.figureUI.tag).Component;
    }
}

css /* scss */ `
  .o_pivot_html_renderer {
    width: 100%;
    border-collapse: collapse;

    &:hover {
      cursor: pointer;
    }

    td,
    th {
      border: 1px solid #dee2e6;
      background-color: #fff;
      padding: 0.3rem;
      white-space: nowrap;

      &:hover {
        filter: brightness(0.9);
      }
    }

    td {
      text-align: right;
    }

    th {
      background-color: #f5f5f5;
      font-weight: bold;
      color: black;
    }

    .o_missing_value {
      color: #46646d;
      background: #e7f2f6;
    }
  }
`;
class PivotHTMLRenderer extends Component {
    static template = "o_spreadsheet.PivotHTMLRenderer";
    static components = { Checkbox };
    static props = {
        pivotId: String,
        onCellClicked: Function,
    };
    pivot = this.env.model.getters.getPivot(this.props.pivotId);
    data = {
        columns: [],
        rows: [],
        values: [],
    };
    state = useState({
        showMissingValuesOnly: false,
    });
    setup() {
        const table = this.pivot.getExpandedTableStructure();
        const formulaId = this.env.model.getters.getPivotFormulaId(this.props.pivotId);
        this.data = {
            columns: this._buildColHeaders(formulaId, table),
            rows: this._buildRowHeaders(formulaId, table),
            values: this._buildValues(formulaId, table),
        };
    }
    get tracker() {
        return this.env.model.getters.getPivotPresenceTracker(this.props.pivotId);
    }
    // ---------------------------------------------------------------------
    // Missing values building
    // ---------------------------------------------------------------------
    /**
     * Retrieve the data to display in the Pivot Table
     * In the case when showMissingValuesOnly is false, the returned value
     * is the complete data
     * In the case when showMissingValuesOnly is true, the returned value is
     * the data which contains only missing values in the rows and cols. In
     * the rows, we also return the parent rows of rows which contains missing
     * values, to give context to the user.
     *
     */
    getTableData() {
        if (!this.state.showMissingValuesOnly) {
            return this.data;
        }
        const colIndexes = this.getColumnsIndexes();
        const rowIndexes = this.getRowsIndexes();
        const columns = this.buildColumnsMissing(colIndexes);
        const rows = this.buildRowsMissing(rowIndexes);
        const values = this.buildValuesMissing(colIndexes, rowIndexes);
        return { columns, rows, values };
    }
    /**
     * Retrieve the parents of the given row
     * ex:
     *  Australia
     *    January
     *    February
     * The parent of "January" is "Australia"
     */
    addRecursiveRow(index) {
        const rows = this.pivot.getExpandedTableStructure().rows;
        const row = [...rows[index].values];
        if (row.length <= 1) {
            return [index];
        }
        row.pop();
        const parentRowIndex = rows.findIndex((r) => JSON.stringify(r.values) === JSON.stringify(row));
        return [index].concat(this.addRecursiveRow(parentRowIndex));
    }
    /**
     * Create the columns to be used, based on the indexes of the columns in
     * which a missing value is present
     *
     */
    buildColumnsMissing(indexes) {
        // columnsMap explode the columns in an array of array of the same
        // size with the index of each column, repeated 'span' times.
        // ex:
        //  | A     | B |
        //  | 1 | 2 | 3 |
        // => [
        //      [0, 0, 1]
        //      [0, 1, 2]
        //    ]
        const columnsMap = [];
        for (const column of this.data.columns) {
            const columnMap = [];
            for (const index in column) {
                for (let i = 0; i < column[index].span; i++) {
                    columnMap.push(parseInt(index, 10));
                }
            }
            columnsMap.push(columnMap);
        }
        // Remove the columns that are not present in indexes
        for (let i = columnsMap[columnsMap.length - 1].length; i >= 0; i--) {
            if (!indexes.includes(i)) {
                for (const columnMap of columnsMap) {
                    columnMap.splice(i, 1);
                }
            }
        }
        // Build the columns
        const columns = [];
        for (const mapIndex in columnsMap) {
            const column = [];
            let index = undefined;
            let span = 1;
            for (let i = 0; i < columnsMap[mapIndex].length; i++) {
                if (index !== columnsMap[mapIndex][i]) {
                    if (index !== undefined) {
                        column.push(Object.assign({}, this.data.columns[mapIndex][index], { span }));
                    }
                    index = columnsMap[mapIndex][i];
                    span = 1;
                }
                else {
                    span++;
                }
            }
            if (index !== undefined) {
                column.push(Object.assign({}, this.data.columns[mapIndex][index], { span }));
            }
            columns.push(column);
        }
        return columns;
    }
    /**
     * Create the rows to be used, based on the indexes of the rows in
     * which a missing value is present.
     */
    buildRowsMissing(indexes) {
        return indexes.map((index) => this.data.rows[index]);
    }
    /**
     * Create the value to be used, based on the indexes of the columns and
     * rows in which a missing value is present.
     */
    buildValuesMissing(colIndexes, rowIndexes) {
        const values = colIndexes.map(() => []);
        for (const row of rowIndexes) {
            for (const col in colIndexes) {
                values[col].push(this.data.values[colIndexes[col]][row]);
            }
        }
        return values;
    }
    getColumnsIndexes() {
        const indexes = new Set();
        for (let i = 0; i < this.data.columns.length; i++) {
            const exploded = [];
            for (let y = 0; y < this.data.columns[i].length; y++) {
                for (let x = 0; x < this.data.columns[i][y].span; x++) {
                    exploded.push(this.data.columns[i][y]);
                }
            }
            for (let y = 0; y < exploded.length; y++) {
                if (exploded[y].isMissing) {
                    indexes.add(y);
                }
            }
        }
        for (let i = 0; i < this.data.columns[this.data.columns.length - 1].length; i++) {
            const values = this.data.values[i];
            if (values.find((x) => x.isMissing)) {
                indexes.add(i);
            }
        }
        return Array.from(indexes).sort((a, b) => a - b);
    }
    getRowsIndexes() {
        const rowIndexes = new Set();
        for (let i = 0; i < this.data.rows.length; i++) {
            if (this.data.rows[i].isMissing) {
                rowIndexes.add(i);
            }
            for (const col of this.data.values) {
                if (col[i].isMissing) {
                    this.addRecursiveRow(i).forEach((x) => rowIndexes.add(x));
                }
            }
        }
        return Array.from(rowIndexes).sort((a, b) => a - b);
    }
    // ---------------------------------------------------------------------
    // Data table creation
    // ---------------------------------------------------------------------
    _buildColHeaders(id, table) {
        const headers = [];
        for (const row of table.columns) {
            const current = [];
            for (const cell of row) {
                const args = [];
                for (let i = 0; i < cell.fields.length; i++) {
                    args.push({ value: cell.fields[i] }, { value: cell.values[i] });
                }
                const domain = this.pivot.parseArgsToPivotDomain(args);
                const locale = this.env.model.getters.getLocale();
                if (domain.at(-1)?.field === "measure") {
                    const { value, format } = this.pivot.getPivotMeasureValue(toString(domain.at(-1).value), domain);
                    current.push({
                        formula: `=PIVOT.HEADER(${generatePivotArgs(id, domain).join(",")})`,
                        value: formatValue(value, { format, locale }),
                        span: cell.width,
                        isMissing: !this.tracker?.isHeaderPresent(domain),
                    });
                }
                else {
                    const { value, format } = this.pivot.getPivotHeaderValueAndFormat(domain);
                    current.push({
                        formula: `=PIVOT.HEADER(${generatePivotArgs(id, domain).join(",")})`,
                        value: formatValue(value, { format, locale }),
                        span: cell.width,
                        isMissing: !this.tracker?.isHeaderPresent(domain),
                    });
                }
            }
            headers.push(current);
        }
        const last = headers[headers.length - 1];
        headers[headers.length - 1] = last.map((cell) => {
            if (!cell.isMissing) {
                cell.style = "color: #756f6f;";
            }
            return cell;
        });
        return headers;
    }
    _buildRowHeaders(id, table) {
        const headers = [];
        for (const row of table.rows) {
            const args = [];
            for (let i = 0; i < row.fields.length; i++) {
                args.push({ value: row.fields[i] }, { value: row.values[i] });
            }
            const domain = this.pivot.parseArgsToPivotDomain(args);
            const { value, format } = this.pivot.getPivotHeaderValueAndFormat(domain);
            const locale = this.env.model.getters.getLocale();
            const cell = {
                formula: `=PIVOT.HEADER(${generatePivotArgs(id, domain).join(",")})`,
                value: formatValue(value, { format, locale }),
                isMissing: !this.tracker?.isHeaderPresent(domain),
            };
            if (row.indent > 1) {
                cell.style = `padding-left: ${row.indent - 1 * 10}px`;
            }
            headers.push(cell);
        }
        return headers;
    }
    _buildValues(id, table) {
        const values = [];
        for (const col of table.columns.at(-1) || []) {
            const current = [];
            const measure = toString(col.values[col.values.length - 1]);
            for (const row of table.rows) {
                const args = [];
                for (let i = 0; i < row.fields.length; i++) {
                    args.push({ value: row.fields[i] }, { value: row.values[i] });
                }
                for (let i = 0; i < col.fields.length - 1; i++) {
                    args.push({ value: col.fields[i] }, { value: col.values[i] });
                }
                const domain = this.pivot.parseArgsToPivotDomain(args);
                const { value, format } = this.pivot.getPivotCellValueAndFormat(measure, domain);
                const locale = this.env.model.getters.getLocale();
                current.push({
                    formula: `=PIVOT.VALUE(${generatePivotArgs(id, domain, measure).join(",")})`,
                    value: formatValue(value, { format, locale }),
                    isMissing: !this.tracker?.isValuePresent(measure, domain),
                });
            }
            values.push(current);
        }
        return values;
    }
}

/**
 * Core plugins handle spreadsheet data.
 * They are responsible to import, export and maintain the spreadsheet
 * persisted state.
 * They should not be concerned about UI parts or transient state.
 */
class CorePlugin extends BasePlugin {
    getters;
    dispatch;
    canDispatch;
    constructor({ getters, stateObserver, range, dispatch, canDispatch }) {
        super(stateObserver);
        range.addRangeProvider(this.adaptRanges.bind(this));
        this.getters = getters;
        this.dispatch = dispatch;
        this.canDispatch = canDispatch;
    }
    // ---------------------------------------------------------------------------
    // 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 sheetId to adapt either range of that sheet specifically, or ranges pointing to that sheet
     * @param sheetName couple of old and new sheet names to adapt ranges pointing to that sheet
     */
    adaptRanges(rangeAdapterFunctions, sheetId, sheetName) { }
    /**
     * Implement this method to clean unused external resources, such as images
     * stored on a server which have been deleted.
     */
    garbageCollectExternalResources() { }
}

/**
 * Formatting plugin.
 *
 * This plugin manages all things related to a cell look:
 * - borders
 */
class BordersPlugin extends CorePlugin {
    static getters = ["getCellBorder", "getBordersColors"];
    borders = {};
    // ---------------------------------------------------------------------------
    // Command Handling
    // ---------------------------------------------------------------------------
    allowDispatch(cmd) {
        switch (cmd.type) {
            case "SET_BORDER":
                return this.checkBordersUnchanged(cmd);
            default:
                return "Success" /* CommandResult.Success */;
        }
    }
    handle(cmd) {
        switch (cmd.type) {
            case "ADD_MERGE":
                for (const zone of cmd.target) {
                    this.addBordersToMerge(cmd.sheetId, zone);
                }
                break;
            case "DUPLICATE_SHEET":
                const borders = this.borders[cmd.sheetId];
                if (borders) {
                    // borders is a sparse 2D array.
                    // map and slice preserve empty values and do not set `undefined` instead
                    const bordersCopy = borders
                        .slice()
                        .map((col) => col?.slice().map((border) => deepCopy(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_BORDERS_ON_TARGET":
                for (const zone of cmd.target) {
                    for (let row = zone.top; row <= zone.bottom; row++) {
                        for (let col = zone.left; col <= zone.right; col++) {
                            this.setBorder(cmd.sheetId, col, row, cmd.border);
                        }
                    }
                }
                break;
            case "SET_ZONE_BORDERS":
                if (cmd.border) {
                    const target = cmd.target.map((zone) => this.getters.expandZone(cmd.sheetId, zone));
                    this.setBorders(cmd.sheetId, target, cmd.border.position, cmd.border.color === ""
                        ? undefined
                        : {
                            style: cmd.border.style || DEFAULT_BORDER_DESC.style,
                            color: cmd.border.color || DEFAULT_BORDER_DESC.color,
                        });
                }
                break;
            case "CLEAR_FORMATTING":
                this.clearBorders(cmd.sheetId, cmd.target);
                break;
            case "REMOVE_COLUMNS_ROWS":
                const elements = [...cmd.elements].sort((a, b) => b - a);
                for (const group of groupConsecutive(elements)) {
                    if (cmd.dimension === "COL") {
                        const zone = this.getters.getColsZone(cmd.sheetId, group[group.length - 1], group[0]);
                        this.clearInsideBorders(cmd.sheetId, [zone]);
                        this.shiftBordersHorizontally(cmd.sheetId, group[0] + 1, -group.length);
                    }
                    else {
                        const zone = this.getters.getRowsZone(cmd.sheetId, group[group.length - 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);
            colLeftOfInsertion = cmd.base - 1;
            colRightOfInsertion = cmd.base + cmd.quantity;
        }
        else {
            this.shiftBordersHorizontally(cmd.sheetId, cmd.base + 1, cmd.quantity);
            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);
            rowAboveInsertion = cmd.base - 1;
            rowBelowInsertion = cmd.base + cmd.quantity;
        }
        else {
            this.shiftBordersVertically(cmd.sheetId, cmd.base + 1, cmd.quantity);
            rowAboveInsertion = cmd.base;
            rowBelowInsertion = cmd.base + cmd.quantity + 1;
        }
        this.ensureRowBorderContinuity(cmd.sheetId, rowAboveInsertion, rowBelowInsertion);
    }
    // ---------------------------------------------------------------------------
    // Getters
    // ---------------------------------------------------------------------------
    getCellBorder({ sheetId, col, row }) {
        const border = this.borders[sheetId]?.[col]?.[row];
        return border?.top || border?.bottom || border?.left || border?.right ? deepCopy(border) : null;
    }
    getBordersColors(sheetId) {
        const colors = [];
        const sheetBorders = this.borders[sheetId];
        if (sheetBorders) {
            for (const borders of sheetBorders.filter(isDefined)) {
                for (const cellBorder of borders) {
                    if (cellBorder) {
                        for (const direction of ["top", "bottom", "left", "right"]) {
                            const color = cellBorder[direction]?.color;
                            if (color) {
                                colors.push(color);
                            }
                        }
                    }
                }
            }
        }
        return colors;
    }
    // ---------------------------------------------------------------------------
    // Private
    // ---------------------------------------------------------------------------
    /**
     * Ensure border continuity between two columns.
     * If the two columns have the same borders (at each row respectively),
     * the same borders are applied to each cell in between.
     */
    ensureColumnBorderContinuity(sheetId, leftColumn, rightColumn) {
        const targetCols = range(leftColumn + 1, rightColumn);
        for (let row = 0; row < this.getters.getNumberRows(sheetId); row++) {
            const leftBorder = this.getCellBorder({ sheetId, col: leftColumn, row });
            const rightBorder = this.getCellBorder({ sheetId, col: rightColumn, row });
            if (leftBorder && rightBorder) {
                const commonSides = this.getCommonSides(leftBorder, rightBorder);
                for (const col of targetCols) {
                    this.addBorder(sheetId, col, row, commonSides);
                }
            }
        }
    }
    /**
     * Ensure border continuity between two rows.
     * If the two rows have the same borders (at each column respectively),
     * the same borders are applied to each cell in between.
     */
    ensureRowBorderContinuity(sheetId, topRow, bottomRow) {
        const targetRows = range(topRow + 1, bottomRow);
        for (let col = 0; col < this.getters.getNumberCols(sheetId); col++) {
            const aboveBorder = this.getCellBorder({ sheetId, col, row: topRow });
            const belowBorder = this.getCellBorder({ sheetId, col, row: bottomRow });
            if (aboveBorder && belowBorder) {
                const commonSides = this.getCommonSides(aboveBorder, belowBorder);
                for (const 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 (const side of ["top", "bottom", "left", "right"]) {
            if (border1[side] && deepEquals(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) {
        const sheetBorders = this.borders[sheetId]?.filter(isDefined);
        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) {
        const borders = this.borders[sheetId];
        if (!borders)
            return;
        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) => {
            this.moveBordersOfColumn(sheetId, col, delta);
        });
    }
    /**
     * 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) {
        const borders = this.borders[sheetId];
        if (!borders)
            return;
        if (delta < 0) {
            this.moveBordersOfRow(sheetId, start, delta, {
                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) => {
            this.moveBordersOfRow(sheetId, row, delta);
        });
    }
    /**
     * 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, { destructive } = { destructive: true }) {
        const borders = this.borders[sheetId];
        if (!borders)
            return;
        this.getColumnsWithBorders(sheetId).forEach((col) => {
            const targetBorder = borders[col]?.[row + delta];
            const movedBorder = borders[col]?.[row];
            this.history.update("borders", sheetId, col, row + delta, destructive ? movedBorder : movedBorder || targetBorder);
            this.history.update("borders", sheetId, col, row, 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, { destructive } = { destructive: true }) {
        const borders = this.borders[sheetId];
        if (!borders)
            return;
        this.getRowsRange(sheetId).forEach((row) => {
            const targetBorder = borders[col + delta]?.[row];
            const movedBorder = borders[col]?.[row];
            this.history.update("borders", sheetId, col + delta, row, destructive ? movedBorder : movedBorder || targetBorder);
            if (destructive) {
                this.history.update("borders", sheetId, col, row, undefined);
            }
        });
    }
    /**
     * Set the borders of a cell.
     * It overrides the current border if override === true.
     */
    setBorder(sheetId, col, row, border, override = true) {
        const maxCol = this.getters.getNumberCols(sheetId) - 1;
        const maxRow = this.getters.getNumberRows(sheetId) - 1;
        if (override || !this.borders[sheetId]?.[col]?.[row]?.left) {
            this.history.update("borders", sheetId, col, row, "left", border?.left);
            if (border?.left &&
                col > 0 &&
                !deepEquals(this.borders[sheetId]?.[col - 1]?.[row]?.right, border?.left)) {
                this.history.update("borders", sheetId, col - 1, row, "right", undefined);
            }
        }
        if (override || !this.borders[sheetId]?.[col]?.[row]?.top) {
            this.history.update("borders", sheetId, col, row, "top", border?.top);
            if (border?.top &&
                row > 0 &&
                !deepEquals(this.borders[sheetId]?.[col]?.[row - 1]?.bottom, border?.top)) {
                this.history.update("borders", sheetId, col, row - 1, "bottom", undefined);
            }
        }
        if (override || !this.borders[sheetId]?.[col]?.[row]?.right) {
            this.history.update("borders", sheetId, col, row, "right", border?.right);
            if (border?.right &&
                col < maxCol &&
                !deepEquals(this.borders[sheetId]?.[col + 1]?.[row]?.left, border?.right)) {
                this.history.update("borders", sheetId, col + 1, row, "left", undefined);
            }
        }
        if (override || !this.borders[sheetId]?.[col]?.[row]?.bottom) {
            this.history.update("borders", sheetId, col, row, "bottom", border?.bottom);
            if (border?.bottom &&
                row < maxRow &&
                !deepEquals(this.borders[sheetId]?.[col]?.[row + 1]?.top, border?.bottom)) {
                this.history.update("borders", sheetId, col, row + 1, "top", undefined);
            }
        }
    }
    /**
     * Remove the borders of a zone
     */
    clearBorders(sheetId, zones, eraseBoundaries = false) {
        const maxCol = this.getters.getNumberCols(sheetId) - 1;
        const maxRow = this.getters.getNumberRows(sheetId) - 1;
        for (const zone of recomputeZones(zones)) {
            for (let row = zone.top; row <= zone.bottom; row++) {
                if (eraseBoundaries) {
                    if (zone.left > 0) {
                        this.history.update("borders", sheetId, zone.left - 1, row, "right", undefined);
                    }
                    if (zone.right < maxCol) {
                        this.history.update("borders", sheetId, zone.right + 1, row, "left", undefined);
                    }
                }
                for (let col = zone.left; col <= zone.right; col++) {
                    this.history.update("borders", sheetId, col, row, undefined);
                    if (eraseBoundaries) {
                        if (zone.top > 0) {
                            this.history.update("borders", sheetId, col, zone.top - 1, "bottom", undefined);
                        }
                        if (zone.bottom < maxRow) {
                            this.history.update("borders", sheetId, col, zone.bottom + 1, "top", undefined);
                        }
                    }
                }
            }
        }
    }
    /**
     * Remove the borders inside of a zone
     */
    clearInsideBorders(sheetId, zones) {
        for (const 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, position, border) {
        if (position === "clear") {
            return this.clearBorders(sheetId, zones, true);
        }
        for (const zone of recomputeZones(zones)) {
            if (position === "all") {
                for (let row = zone.top; row <= zone.bottom; row++) {
                    for (let col = zone.left; col <= zone.right; col++) {
                        this.addBorder(sheetId, col, row, {
                            top: border,
                            right: border,
                            bottom: border,
                            left: border,
                        });
                    }
                }
            }
            if (position === "h" || position === "hv") {
                if (zone.top === zone.bottom) {
                    continue;
                }
                for (let col = zone.left; col <= zone.right; col++) {
                    this.addBorder(sheetId, col, zone.top, { bottom: border });
                    for (let row = zone.top + 1; row < zone.bottom; row++) {
                        this.addBorder(sheetId, col, row, { top: border, bottom: border });
                    }
                    this.addBorder(sheetId, col, zone.bottom, { top: border });
                }
            }
            if (position === "v" || position === "hv") {
                if (zone.left === zone.right) {
                    continue;
                }
                for (let row = zone.top; row <= zone.bottom; row++) {
                    this.addBorder(sheetId, zone.left, row, { right: border });
                    for (let col = zone.left + 1; col < zone.right; col++) {
                        this.addBorder(sheetId, col, row, { left: border, right: border });
                    }
                    this.addBorder(sheetId, zone.right, row, { left: border });
                }
            }
            if (position === "left" || position === "external") {
                for (let row = zone.top; row <= zone.bottom; row++) {
                    this.addBorder(sheetId, zone.left, row, { left: border });
                }
            }
            if (position === "right" || position === "external") {
                for (let row = zone.top; row <= zone.bottom; row++) {
                    this.addBorder(sheetId, zone.right, row, { right: border });
                }
            }
            if (position === "top" || position === "external") {
                for (let col = zone.left; col <= zone.right; col++) {
                    this.addBorder(sheetId, col, zone.top, { top: border });
                }
            }
            if (position === "bottom" || position === "external") {
                for (let col = zone.left; col <= zone.right; col++) {
                    this.addBorder(sheetId, col, zone.bottom, { bottom: border });
                }
            }
        }
    }
    /**
     * Compute the borders to add to the given zone merged.
     */
    addBordersToMerge(sheetId, zone) {
        const { left, right, top, bottom } = zone;
        const bordersTopLeft = this.getCellBorder({ sheetId, col: left, row: top });
        const bordersBottomRight = this.getCellBorder({ sheetId, col: right, row: bottom });
        this.clearBorders(sheetId, [zone]);
        if (bordersTopLeft?.top) {
            this.setBorders(sheetId, [{ ...zone, bottom: top }], "top", bordersTopLeft.top);
        }
        if (bordersTopLeft?.left) {
            this.setBorders(sheetId, [{ ...zone, right: left }], "left", bordersTopLeft.left);
        }
        if (bordersBottomRight?.bottom) {
            this.setBorders(sheetId, [{ ...zone, top: bottom }], "bottom", bordersBottomRight.bottom);
        }
        else if (bordersTopLeft?.bottom) {
            this.setBorders(sheetId, [{ ...zone, top: bottom }], "bottom", bordersTopLeft.bottom);
        }
        if (bordersBottomRight?.right) {
            this.setBorders(sheetId, [{ ...zone, left: right }], "right", bordersBottomRight.right);
        }
        else if (bordersTopLeft?.right) {
            this.setBorders(sheetId, [{ ...zone, left: right }], "right", bordersTopLeft.right);
        }
    }
    checkBordersUnchanged(cmd) {
        const currentBorder = this.getCellBorder(cmd);
        const areAllNewBordersUndefined = !cmd.border?.bottom && !cmd.border?.left && !cmd.border?.right && !cmd.border?.top;
        if ((!currentBorder && areAllNewBordersUndefined) || deepEquals(currentBorder, cmd.border)) {
            return "NoChanges" /* CommandResult.NoChanges */;
        }
        return "Success" /* CommandResult.Success */;
    }
    // ---------------------------------------------------------------------------
    // Import/Export
    // ---------------------------------------------------------------------------
    import(data) {
        // Borders
        if (Object.keys(data.borders || {}).length) {
            for (const sheet of data.sheets) {
                for (const [position, borderId] of iterateItemIdsPositions(sheet.id, sheet.borders)) {
                    const { sheetId, col, row } = position;
                    const border = data.borders[borderId];
                    this.setBorder(sheetId, col, row, border, false);
                }
            }
        }
        // Merges
        for (const sheetData of data.sheets) {
            if (sheetData.merges) {
                for (const merge of sheetData.merges) {
                    this.addBordersToMerge(sheetData.id, toZone(merge));
                }
            }
        }
    }
    export(data) {
        const borders = {};
        for (const sheet of data.sheets) {
            const positionsByBorder = {};
            for (let col = 0; col < sheet.colNumber; col++) {
                for (let row = 0; row < sheet.rowNumber; row++) {
                    const border = this.getCellBorder({ sheetId: sheet.id, col, row });
                    if (border) {
                        const borderId = getItemId(border, borders);
                        const position = { sheetId: sheet.id, col, row };
                        positionsByBorder[borderId] ??= [];
                        positionsByBorder[borderId].push(position);
                    }
                }
            }
            sheet.borders = groupItemIdsByZones(positionsByBorder);
        }
        data.borders = borders;
    }
    exportForExcel(data) {
        this.export(data);
    }
}

/**
 * Core Plugin
 *
 * This is the most fundamental of all plugins. It defines how to interact with
 * cell and sheet content.
 */
class CellPlugin extends CorePlugin {
    static getters = [
        "zoneToXC",
        "getCells",
        "getTranslatedCellFormula",
        "getCellStyle",
        "getCellById",
        "getFormulaString",
        "getFormulaMovedInSheet",
    ];
    nextId = 1;
    cells = {};
    adaptRanges({ applyChange }, sheetId, sheetName) {
        for (const sheet of Object.keys(this.cells)) {
            for (const cell of Object.values(this.cells[sheet] || {})) {
                if (cell.isFormula) {
                    for (const range of cell.compiledFormula.dependencies) {
                        if (range.sheetId === sheetId || range.invalidSheetName === sheetName.old) {
                            const change = applyChange(range);
                            if (change.changeType !== "NONE") {
                                this.history.update("cells", sheet, cell.id, "compiledFormula", "dependencies", cell.compiledFormula.dependencies.indexOf(range), change.range);
                            }
                        }
                    }
                }
            }
        }
    }
    // ---------------------------------------------------------------------------
    // Command Handling
    // ---------------------------------------------------------------------------
    allowDispatch(cmd) {
        switch (cmd.type) {
            case "UPDATE_CELL":
                return this.checkValidations(cmd, this.checkCellOutOfSheet, this.checkUselessUpdateCell);
            case "CLEAR_CELL":
                return this.checkValidations(cmd, this.checkCellOutOfSheet, this.checkUselessClearCell);
            case "UPDATE_CELL_POSITION":
                return !cmd.cellId || this.cells[cmd.sheetId]?.[cmd.cellId]
                    ? "Success" /* CommandResult.Success */
                    : "InvalidCellId" /* CommandResult.InvalidCellId */;
            default:
                return "Success" /* CommandResult.Success */;
        }
    }
    handle(cmd) {
        switch (cmd.type) {
            case "SET_FORMATTING":
                if ("style" in cmd) {
                    this.setStyle(cmd.sheetId, cmd.target, cmd.style);
                }
                if ("format" in cmd && cmd.format !== undefined) {
                    this.setFormatter(cmd.sheetId, cmd.target, cmd.format);
                }
                break;
            case "CLEAR_FORMATTING":
                this.clearFormatting(cmd.sheetId, cmd.target);
                break;
            case "ADD_COLUMNS_ROWS":
                if (cmd.dimension === "COL") {
                    this.handleAddColumnsRows(cmd, this.copyColumnStyle.bind(this));
                }
                else {
                    this.handleAddColumnsRows(cmd, this.copyRowStyle.bind(this));
                }
                break;
            case "UPDATE_CELL":
                this.updateCell(cmd.sheetId, cmd.col, cmd.row, cmd);
                break;
            case "CLEAR_CELL":
                this.dispatch("UPDATE_CELL", {
                    sheetId: cmd.sheetId,
                    col: cmd.col,
                    row: cmd.row,
                    content: "",
                    style: null,
                    format: "",
                });
                break;
            case "CLEAR_CELLS":
                this.clearCells(cmd.sheetId, cmd.target);
                break;
            case "DELETE_CONTENT":
                this.clearZones(cmd.sheetId, cmd.target);
                break;
            case "DELETE_SHEET": {
                this.history.update("cells", cmd.sheetId, undefined);
            }
        }
    }
    clearZones(sheetId, zones) {
        for (const zone of recomputeZones(zones)) {
            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 });
                    if (cell?.isFormula || cell?.content) {
                        this.dispatch("UPDATE_CELL", {
                            sheetId: sheetId,
                            content: "",
                            col,
                            row,
                        });
                    }
                }
            }
        }
    }
    /**
     * Set a format to all the cells in a zone
     */
    setFormatter(sheetId, zones, format) {
        for (const zone of recomputeZones(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 (const zone of recomputeZones(zones)) {
            for (let col = zone.left; col <= zone.right; col++) {
                for (let row = zone.top; row <= zone.bottom; row++) {
                    this.dispatch("UPDATE_CELL", {
                        sheetId,
                        col,
                        row,
                        style: null,
                        format: "",
                    });
                }
            }
        }
    }
    /**
     * Clear the styles, the format and the content of zones
     */
    clearCells(sheetId, zones) {
        for (const zone of zones) {
            for (let col = zone.left; col <= zone.right; col++) {
                for (let row = zone.top; row <= zone.bottom; row++) {
                    this.dispatch("UPDATE_CELL", {
                        sheetId: sheetId,
                        col,
                        row,
                        content: "",
                        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 (const sheet of data.sheets) {
            const sheetId = sheet.id;
            const cellsData = new PositionMap();
            // cells content
            for (const xc in sheet.cells) {
                if (sheet.cells[xc]) {
                    const { col, row } = toCartesian(xc);
                    const position = { sheetId: sheet.id, col, row };
                    cellsData.set(position, { content: sheet.cells[xc] });
                }
            }
            // cells style and format
            for (const [cellProperty, valuesByZones] of [
                ["style", sheet.styles],
                ["format", sheet.formats],
            ]) {
                for (const [position, itemId] of iterateItemIdsPositions(sheet.id, valuesByZones)) {
                    const cellData = cellsData.get(position);
                    if (cellData) {
                        cellData[cellProperty] = itemId;
                    }
                    else {
                        cellsData.set(position, { [cellProperty]: itemId });
                    }
                }
            }
            for (const position of cellsData.keysForSheet(sheetId)) {
                const cellData = cellsData.get(position);
                if (cellData?.content || cellData?.format || cellData?.style) {
                    const cell = this.importCell(sheet.id, cellData?.content, cellData?.style ? data.styles[cellData?.style] : undefined, cellData?.format ? data.formats[cellData?.format] : undefined);
                    this.history.update("cells", sheet.id, cell.id, cell);
                    this.dispatch("UPDATE_CELL_POSITION", {
                        cellId: cell.id,
                        ...position,
                    });
                }
            }
        }
    }
    export(data) {
        const styles = {};
        const formats = {};
        for (const _sheet of data.sheets) {
            const positionsByStyle = [];
            const positionsByFormat = [];
            const cells = {};
            const positions = Object.keys(this.cells[_sheet.id] || {})
                .map((cellId) => this.getters.getCellPosition(cellId))
                .sort((a, b) => (a.col === b.col ? a.row - b.row : a.col - b.col));
            for (const position of positions) {
                const cell = this.getters.getCell(position);
                const xc = toXC(position.col, position.row);
                const style = this.extractCustomStyle(cell);
                if (Object.keys(style).length) {
                    const styleId = getItemId(style, styles);
                    positionsByStyle[styleId] ??= [];
                    positionsByStyle[styleId].push(position);
                }
                if (cell.format) {
                    const formatId = getItemId(cell.format, formats);
                    positionsByFormat[formatId] ??= [];
                    positionsByFormat[formatId].push(position);
                }
                if (cell.content) {
                    cells[xc] = cell.content;
                }
            }
            _sheet.styles = groupItemIdsByZones(positionsByStyle);
            _sheet.formats = groupItemIdsByZones(positionsByFormat);
            _sheet.cells = cells;
        }
        data.styles = styles;
        data.formats = formats;
    }
    importCell(sheetId, content, style, format) {
        const cellId = this.getNextUid();
        return this.createCell(cellId, content || "", format, style, sheetId);
    }
    exportForExcel(data) {
        this.export(data);
        const incompatibleIds = [];
        for (const formatId in data.formats || []) {
            if (!isExcelCompatible(data.formats[formatId])) {
                incompatibleIds.push(Number(formatId));
                delete data.formats[formatId];
            }
        }
        if (incompatibleIds.length) {
            for (const sheet of data.sheets) {
                for (const zoneXc in sheet.formats) {
                    const formatId = sheet.formats[zoneXc];
                    if (formatId && incompatibleIds.includes(formatId)) {
                        delete sheet.formats[zoneXc];
                    }
                }
            }
        }
    }
    extractCustomStyle(cell) {
        const cleanedStyle = { ...cell.style };
        const defaultStyle = isNumber(cell.content, DEFAULT_LOCALE)
            ? DEFAULT_NUMBER_STYLE
            : DEFAULT_STYLE;
        for (const property in cleanedStyle) {
            if ((property !== "align" || !cell.isFormula) &&
                cleanedStyle[property] === defaultStyle[property]) {
                delete cleanedStyle[property];
            }
        }
        return cleanedStyle;
    }
    // ---------------------------------------------------------------------------
    // GETTERS
    // ---------------------------------------------------------------------------
    getCells(sheetId) {
        return this.cells[sheetId] || {};
    }
    /**
     * get a cell by ID. Used in evaluation when evaluating an async cell, we need to be able to find it back after
     * starting an async evaluation even if it has been moved or re-allocated
     */
    getCellById(cellId) {
        // this must be as fast as possible
        const position = this.getters.getCellPosition(cellId);
        const sheet = this.cells[position.sheetId];
        return sheet[cellId];
    }
    /*
     * Reconstructs the original formula string based on new dependencies
     */
    getFormulaString(sheetId, tokens, dependencies, useBoundedReference = false) {
        if (!dependencies.length) {
            return concat$1(tokens.map((token) => token.value));
        }
        let rangeIndex = 0;
        return concat$1(tokens.map((token) => {
            if (token.type === "REFERENCE") {
                const range = dependencies[rangeIndex++];
                return this.getters.getRangeString(range, sheetId, { useBoundedReference });
            }
            return token.value;
        }));
    }
    /*
     * Constructs a formula string based on an initial formula and a translation vector
     */
    getTranslatedCellFormula(sheetId, offsetX, offsetY, tokens) {
        const adaptedDependencies = this.getters.createAdaptedRanges(compileTokens(tokens).dependencies.map((d) => this.getters.getRangeFromSheetXC(sheetId, d)), offsetX, offsetY, sheetId);
        return this.getFormulaString(sheetId, tokens, adaptedDependencies);
    }
    getFormulaMovedInSheet(originSheetId, targetSheetId, tokens) {
        const dependencies = compileTokens(tokens).dependencies.map((d) => this.getters.getRangeFromSheetXC(originSheetId, d));
        const adaptedDependencies = this.getters.removeRangesSheetPrefix(targetSheetId, dependencies);
        return this.getFormulaString(targetSheetId, tokens, adaptedDependencies);
    }
    getCellStyle(position) {
        return this.getters.getCell(position)?.style || {};
    }
    /**
     * Converts a zone to a XC coordinate system
     *
     * The conversion also treats merges as one single cell
     *
     * Examples:
     * {top:0,left:0,right:0,bottom:0} ==> A1
     * {top:0,left:0,right:1,bottom:1} ==> A1:B2
     *
     * if A1:B2 is a merge:
     * {top:0,left:0,right:1,bottom:1} ==> A1
     * {top:1,left:0,right:1,bottom:2} ==> A1:B3
     *
     * if A1:B2 and A4:B5 are merges:
     * {top:1,left:0,right:1,bottom:3} ==> A1:A5
     */
    zoneToXC(sheetId, zone, fixedParts = [{ colFixed: false, rowFixed: false }]) {
        zone = this.getters.expandZone(sheetId, zone);
        const topLeft = toXC(zone.left, zone.top, fixedParts[0]);
        const botRight = toXC(zone.right, zone.bottom, fixedParts.length > 1 ? fixedParts[1] : fixedParts[0]);
        const cellTopLeft = this.getters.getMainCellPosition({
            sheetId,
            col: zone.left,
            row: zone.top,
        });
        const cellBotRight = this.getters.getMainCellPosition({
            sheetId,
            col: zone.right,
            row: zone.bottom,
        });
        const sameCell = cellTopLeft.col === cellBotRight.col && cellTopLeft.row === cellBotRight.row;
        if (topLeft !== botRight && !sameCell) {
            return topLeft + ":" + botRight;
        }
        return topLeft;
    }
    setStyle(sheetId, target, style) {
        for (const zone of recomputeZones(target)) {
            for (let col = zone.left; col <= zone.right; col++) {
                for (let row = zone.top; row <= zone.bottom; row++) {
                    const cell = this.getters.getCell({ sheetId, col, row });
                    this.dispatch("UPDATE_CELL", {
                        sheetId,
                        col,
                        row,
                        style: style ? { ...cell?.style, ...style } : undefined,
                    });
                }
            }
        }
    }
    /**
     * Copy the style of one column to other columns.
     */
    copyColumnStyle(sheetId, refColumn, targetCols) {
        for (let row = 0; row < this.getters.getNumberRows(sheetId); row++) {
            const format = this.getFormat(sheetId, refColumn, row);
            if (format.style || format.format) {
                for (const 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 (const row of targetRows) {
                    this.dispatch("UPDATE_CELL", { sheetId, col, row, ...format });
                }
            }
        }
    }
    /**
     * gets the currently used style/border of a cell based on it's coordinates
     */
    getFormat(sheetId, col, row) {
        const format = {};
        const position = this.getters.getMainCellPosition({ sheetId, col, row });
        const cell = this.getters.getCell(position);
        if (cell) {
            if (cell.style) {
                format["style"] = cell.style;
            }
            if (cell.format) {
                format["format"] = cell.format;
            }
        }
        return format;
    }
    getNextUid() {
        const id = this.nextId.toString();
        this.history.update("nextId", this.nextId + 1);
        return id;
    }
    updateCell(sheetId, col, row, after) {
        const before = this.getters.getCell({ sheetId, col, row });
        const hasContent = after.content !== undefined || "formula" in after;
        // Compute the new cell properties
        const afterContent = hasContent
            ? replaceNewLines(after?.content || undefined)
            : before?.content || "";
        let style;
        if (after.style !== undefined) {
            style = after.style || undefined;
        }
        else {
            style = before ? before.style : undefined;
        }
        const format = after.format !== undefined ? after.format : before && before.format;
        /* Read the following IF as:
         * we need to remove the cell if it is completely empty, but we can know if it completely empty if:
         * - the command says the new content is empty and has no border/format/style
         * - the command has no content property, in this case
         *     - either there wasn't a cell at this place and the command says border/format/style is empty
         *     - or there was a cell at this place, but it's an empty cell and the command says border/format/style is empty
         *  */
        if (((hasContent && !afterContent && !after.formula) ||
            (!hasContent && (!before || before.content === ""))) &&
            !style &&
            !format) {
            if (before) {
                this.history.update("cells", sheetId, before.id, undefined);
                this.dispatch("UPDATE_CELL_POSITION", {
                    cellId: undefined,
                    col,
                    row,
                    sheetId,
                });
            }
            return;
        }
        const cellId = before?.id || this.getNextUid();
        const cell = this.createCell(cellId, afterContent, format, style, sheetId);
        this.history.update("cells", sheetId, cell.id, cell);
        this.dispatch("UPDATE_CELL_POSITION", { cellId: cell.id, col, row, sheetId });
    }
    createCell(id, content, format, style, sheetId) {
        if (!content.startsWith("=")) {
            return this.createLiteralCell(id, content, format, style);
        }
        return this.createFormulaCell(id, content, format, style, sheetId);
    }
    createLiteralCell(id, content, format, style) {
        const locale = this.getters.getLocale();
        const parsedValue = parseLiteral(content, locale);
        format =
            format ||
                (typeof parsedValue === "number"
                    ? detectDateFormat(content, locale) || detectNumberFormat(content)
                    : undefined);
        if (!isTextFormat(format) && !isEvaluationError(content)) {
            content = toString(parsedValue);
        }
        return {
            id,
            content,
            style,
            format,
            isFormula: false,
            parsedValue,
        };
    }
    createFormulaCell(id, content, format, style, sheetId) {
        const compiledFormula = compile(content);
        if (compiledFormula.dependencies.length) {
            return this.createFormulaCellWithDependencies(id, compiledFormula, format, style, sheetId);
        }
        return {
            id,
            content,
            style,
            format,
            isFormula: true,
            compiledFormula: {
                ...compiledFormula,
                dependencies: [],
            },
        };
    }
    /**
     * Create a new formula cell with the content
     * being a computed property to rebuild the dependencies XC.
     */
    createFormulaCellWithDependencies(id, compiledFormula, format, style, sheetId) {
        const dependencies = [];
        for (const xc of compiledFormula.dependencies) {
            dependencies.push(this.getters.getRangeFromSheetXC(sheetId, xc));
        }
        return new FormulaCellWithDependencies(id, compiledFormula, format, style, dependencies, sheetId, this.getters.getRangeString);
    }
    checkCellOutOfSheet(cmd) {
        const { sheetId, col, row } = cmd;
        const sheet = this.getters.tryGetSheet(sheetId);
        if (!sheet)
            return "InvalidSheetId" /* CommandResult.InvalidSheetId */;
        const sheetZone = this.getters.getSheetZone(sheetId);
        return isInside(col, row, sheetZone) ? "Success" /* CommandResult.Success */ : "TargetOutOfSheet" /* CommandResult.TargetOutOfSheet */;
    }
    checkUselessClearCell(cmd) {
        const cell = this.getters.getCell(cmd);
        if (!cell)
            return "NoChanges" /* CommandResult.NoChanges */;
        if (!cell.content && !cell.style && !cell.format) {
            return "NoChanges" /* CommandResult.NoChanges */;
        }
        return "Success" /* CommandResult.Success */;
    }
    checkUselessUpdateCell(cmd) {
        const cell = this.getters.getCell(cmd);
        const hasContent = "content" in cmd || "formula" in cmd;
        const hasStyle = "style" in cmd;
        const hasFormat = "format" in cmd;
        if ((!hasContent || cell?.content === cmd.content) &&
            (!hasStyle || deepEquals(cell?.style, cmd.style)) &&
            (!hasFormat || cell?.format === cmd.format)) {
            return "NoChanges" /* CommandResult.NoChanges */;
        }
        return "Success" /* CommandResult.Success */;
    }
}
class FormulaCellWithDependencies {
    id;
    format;
    style;
    sheetId;
    getRangeString;
    isFormula = true;
    compiledFormula;
    constructor(id, compiledFormula, format, style, dependencies, sheetId, getRangeString) {
        this.id = id;
        this.format = format;
        this.style = style;
        this.sheetId = sheetId;
        this.getRangeString = getRangeString;
        let rangeIndex = 0;
        const tokens = compiledFormula.tokens.map((token) => {
            if (token.type === "REFERENCE") {
                const index = rangeIndex++;
                return new RangeReferenceToken(dependencies, index, this.sheetId, this.getRangeString);
            }
            return token;
        });
        this.compiledFormula = {
            ...compiledFormula,
            dependencies,
            tokens,
        };
    }
    get content() {
        return concat$1(this.compiledFormula.tokens.map((token) => token.value));
    }
    get contentWithFixedReferences() {
        let rangeIndex = 0;
        return concat$1(this.compiledFormula.tokens.map((token) => {
            if (token.type === "REFERENCE") {
                const index = rangeIndex++;
                return this.getRangeString(this.compiledFormula.dependencies[index], this.sheetId, {
                    useBoundedReference: true,
                });
            }
            return token.value;
        }));
    }
}
class RangeReferenceToken {
    ranges;
    rangeIndex;
    sheetId;
    getRangeString;
    type = "REFERENCE";
    constructor(ranges, rangeIndex, sheetId, getRangeString) {
        this.ranges = ranges;
        this.rangeIndex = rangeIndex;
        this.sheetId = sheetId;
        this.getRangeString = getRangeString;
    }
    get value() {
        const range = this.ranges[this.rangeIndex];
        return this.getRangeString(range, this.sheetId);
    }
}

class ChartPlugin extends CorePlugin {
    static getters = [
        "isChartDefined",
        "getChartDefinition",
        "getChartType",
        "getChartIds",
        "getChart",
        "getFigureIdFromChartId",
        "getContextCreationChart",
    ];
    charts = {};
    createChart = chartFactory(this.getters);
    validateChartDefinition = (cmd) => validateChartDefinition(this, cmd.definition);
    adaptRanges(rangeAdapters) {
        for (const [chartId, chart] of Object.entries(this.charts)) {
            if (!chart) {
                continue;
            }
            const newChart = chart.chart.updateRanges(rangeAdapters);
            this.history.update("charts", chartId, newChart ? { figureId: chart.figureId, chart: newChart } : undefined);
        }
    }
    // ---------------------------------------------------------------------------
    // Command Handling
    // ---------------------------------------------------------------------------
    allowDispatch(cmd) {
        switch (cmd.type) {
            case "CREATE_CHART":
                return this.checkValidations(cmd, this.chainValidations(this.checkFigureArguments, this.validateChartDefinition, this.checkChartDuplicate));
            case "UPDATE_CHART":
                return this.checkValidations(cmd, this.chainValidations(this.validateChartDefinition, this.checkChartExists, this.checkChartChanged));
            case "DELETE_CHART":
                return this.checkChartExists(cmd);
            default:
                return "Success" /* CommandResult.Success */;
        }
    }
    handle(cmd) {
        switch (cmd.type) {
            case "CREATE_CHART":
                const { col, row, offset, size, sheetId, figureId } = cmd;
                // If figure position is not defined, it means that the figure already exist (see allowDispatch)
                if (!this.getters.getFigure(sheetId, figureId) &&
                    offset !== undefined &&
                    col !== undefined &&
                    row !== undefined) {
                    this.addFigure(figureId, sheetId, col, row, offset, size);
                }
                this.addChart(cmd.figureId, cmd.chartId, cmd.definition);
                break;
            case "UPDATE_CHART": {
                this.addChart(cmd.figureId, cmd.chartId, cmd.definition);
                break;
            }
            case "DUPLICATE_SHEET": {
                for (const chartId of this.getChartIds(cmd.sheetId)) {
                    const { chart, figureId } = this.charts[chartId] || {};
                    if (!chart || !figureId) {
                        continue;
                    }
                    const fig = this.getters.getFigure(cmd.sheetId, figureId);
                    if (!fig) {
                        continue;
                    }
                    const figureIdBase = figureId.split(FIGURE_ID_SPLITTER).pop();
                    const duplicatedFigureId = `${cmd.sheetIdTo}${FIGURE_ID_SPLITTER}${figureIdBase}`;
                    const chartIdBase = chartId.split(FIGURE_ID_SPLITTER).pop();
                    const duplicatedChartId = `${cmd.sheetIdTo}${FIGURE_ID_SPLITTER}${chartIdBase}`;
                    const newChart = chart.duplicateInDuplicatedSheet(cmd.sheetIdTo);
                    if (newChart) {
                        this.dispatch("CREATE_CHART", {
                            figureId: duplicatedFigureId,
                            chartId: duplicatedChartId,
                            col: fig.col,
                            row: fig.row,
                            offset: fig.offset,
                            size: { width: fig.width, height: fig.height },
                            definition: newChart.getDefinition(),
                            sheetId: cmd.sheetIdTo,
                        });
                    }
                }
                break;
            }
            case "DELETE_FIGURE":
                for (const chartId in this.charts) {
                    if (this.charts[chartId]?.figureId === cmd.figureId) {
                        this.dispatch("DELETE_CHART", { chartId, sheetId: cmd.sheetId });
                    }
                }
                break;
            case "DELETE_CHART":
                if (this.isChartDefined(cmd.chartId)) {
                    this.history.update("charts", cmd.chartId, undefined);
                }
                break;
            case "DELETE_SHEET":
                for (const id of this.getChartIds(cmd.sheetId)) {
                    this.history.update("charts", id, undefined);
                }
                break;
        }
    }
    // ---------------------------------------------------------------------------
    // Getters
    // ---------------------------------------------------------------------------
    getContextCreationChart(chartId) {
        return this.charts[chartId]?.chart.getContextCreation();
    }
    getChart(chartId) {
        return this.charts[chartId]?.chart;
    }
    getFigureIdFromChartId(chartId) {
        if (!this.charts[chartId]) {
            throw new Error(`Chart with id ${chartId} does not exist.`);
        }
        return this.charts[chartId].figureId;
    }
    getChartType(chartId) {
        const type = this.charts[chartId]?.chart.type;
        if (!type) {
            throw new Error("Chart not defined.");
        }
        return type;
    }
    isChartDefined(chartId) {
        return chartId in this.charts && this.charts !== undefined;
    }
    getChartIds(sheetId) {
        return Object.entries(this.charts)
            .filter(([, chart]) => chart?.chart.sheetId === sheetId)
            .map(([id]) => id);
    }
    getChartDefinition(chartId) {
        const definition = this.charts[chartId]?.chart.getDefinition();
        if (!definition) {
            throw new Error(`There is no chart with the given id: ${chartId}`);
        }
        return definition;
    }
    // ---------------------------------------------------------------------------
    // Import/Export
    // ---------------------------------------------------------------------------
    import(data) {
        for (const sheet of data.sheets) {
            if (sheet.figures) {
                for (const 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") {
                        const chartId = figure.data.chartId;
                        const chart = this.createChart(figure.id, figure.data, sheet.id);
                        this.charts[chartId] = { chart, figureId: figure.id };
                    }
                    else if (figure.tag === "carousel") {
                        for (const chartId in figure.data.chartDefinitions || {}) {
                            const chartDefinition = figure.data.chartDefinitions[chartId];
                            const chart = this.createChart(figure.id, chartDefinition, sheet.id);
                            this.charts[chartId] = { chart, figureId: figure.id };
                        }
                    }
                }
            }
        }
    }
    export(data) {
        if (data.sheets) {
            for (const sheet of data.sheets) {
                // TODO This code is false, if two plugins want to insert figures on the sheet, it will crash !
                const sheetFigures = this.getters.getFigures(sheet.id);
                const figures = [];
                for (const sheetFigure of sheetFigures) {
                    const figure = sheetFigure;
                    const chartId = Object.keys(this.charts).find((chartId) => this.charts[chartId]?.figureId === sheetFigure.id);
                    if (figure && figure.tag === "chart" && chartId) {
                        const data = this.charts[chartId]?.chart.getDefinition();
                        if (data) {
                            figure.data = { ...data, chartId };
                            figures.push(figure);
                        }
                    }
                    else if (figure && figure.tag === "carousel") {
                        const chartIds = Object.keys(this.charts).filter((chartId) => this.charts[chartId]?.figureId === sheetFigure.id);
                        const data = {};
                        for (const chartId of chartIds) {
                            data[chartId] = this.charts[chartId]?.chart.getDefinition();
                        }
                        figure.data = { chartDefinitions: data };
                        figures.push(figure);
                    }
                    else {
                        figures.push(figure);
                    }
                }
                sheet.figures = figures;
            }
        }
    }
    // ---------------------------------------------------------------------------
    // Private
    // ---------------------------------------------------------------------------
    /**
     * Add a figure with tag chart with the given id at the given position
     */
    addFigure(figureId, sheetId, col, row, offset, size = {
        width: DEFAULT_FIGURE_WIDTH,
        height: DEFAULT_FIGURE_HEIGHT,
    }) {
        this.dispatch("CREATE_FIGURE", {
            sheetId,
            figureId,
            col,
            row,
            offset,
            size,
            tag: "chart",
        });
    }
    /**
     * Add a chart in the local state. If a chart already exists, this chart is
     * replaced
     */
    addChart(figureId, chartId, definition) {
        const sheetId = this.getters.getFigureSheetId(figureId);
        if (sheetId) {
            const chart = this.createChart(figureId, definition, sheetId);
            this.history.update("charts", chartId, { figureId, chart });
        }
    }
    checkChartDuplicate(cmd) {
        return this.isChartDefined(cmd.chartId)
            ? "DuplicatedChartId" /* CommandResult.DuplicatedChartId */
            : "Success" /* CommandResult.Success */;
    }
    checkChartExists(cmd) {
        return this.isChartDefined(cmd.chartId)
            ? "Success" /* CommandResult.Success */
            : "ChartDoesNotExist" /* CommandResult.ChartDoesNotExist */;
    }
    checkChartChanged(cmd) {
        if (cmd.figureId !== this.charts[cmd.chartId]?.figureId) {
            return "Success" /* CommandResult.Success */;
        }
        return deepEquals(this.getChartDefinition(cmd.chartId), cmd.definition)
            ? "NoChanges" /* CommandResult.NoChanges */
            : "Success" /* CommandResult.Success */;
    }
    /** If the command is meant to create a new figure, the position & offset argument need to be defined in the command */
    checkFigureArguments(cmd) {
        if (this.getters.getFigure(cmd.sheetId, cmd.figureId)) {
            return "Success" /* CommandResult.Success */;
        }
        return cmd.offset !== undefined && cmd.col !== undefined && cmd.row !== undefined
            ? "Success" /* CommandResult.Success */
            : "MissingFigureArguments" /* CommandResult.MissingFigureArguments */;
    }
}

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

class DataValidationPlugin extends CorePlugin {
    static getters = [
        "cellHasListDataValidationIcon",
        "getDataValidationRule",
        "getDataValidationRules",
        "getValidationRuleForCell",
    ];
    rules = {};
    adaptRanges(rangeAdapters, sheetId) {
        this.adaptDVRanges(sheetId, rangeAdapters);
        this.adaptDVFormulas(rangeAdapters);
    }
    adaptDVFormulas({ adaptFormulaString }) {
        for (const sheetId in this.rules) {
            const rules = this.rules[sheetId];
            for (let ruleIndex = rules.length - 1; ruleIndex >= 0; ruleIndex--) {
                const rule = this.rules[sheetId][ruleIndex];
                for (let valueIndex = 0; valueIndex < rule.criterion.values.length; valueIndex++) {
                    const value = adaptFormulaString(sheetId, rule.criterion.values[valueIndex]);
                    this.history.update("rules", sheetId, ruleIndex, "criterion", "values", valueIndex, value);
                }
            }
        }
    }
    adaptDVRanges(sheetId, { applyChange }) {
        const rules = this.rules[sheetId];
        for (let ruleIndex = rules.length - 1; ruleIndex >= 0; ruleIndex--) {
            const rule = this.rules[sheetId][ruleIndex];
            for (let rangeIndex = rule.ranges.length - 1; rangeIndex >= 0; rangeIndex--) {
                const range = rule.ranges[rangeIndex];
                const change = applyChange(range);
                switch (change.changeType) {
                    case "REMOVE":
                        if (rule.ranges.length === 1) {
                            this.removeDataValidationRule(sheetId, rule.id);
                        }
                        else {
                            const copy = rule.ranges.slice();
                            copy.splice(rangeIndex, 1);
                            this.history.update("rules", sheetId, ruleIndex, "ranges", copy);
                        }
                        break;
                    case "RESIZE":
                    case "MOVE":
                    case "CHANGE":
                        this.history.update("rules", sheetId, ruleIndex, "ranges", rangeIndex, change.range);
                        break;
                }
            }
        }
    }
    allowDispatch(cmd) {
        switch (cmd.type) {
            case "ADD_DATA_VALIDATION_RULE":
                if (!this.getters.tryGetSheet(cmd.sheetId)) {
                    return "InvalidSheetId" /* CommandResult.InvalidSheetId */;
                }
                if (cmd.ranges.some((rangeData) => !this.getters.tryGetSheet(rangeData._sheetId))) {
                    return "InvalidSheetId" /* CommandResult.InvalidSheetId */;
                }
                return this.checkValidations(cmd, this.chainValidations(this.checkEmptyRange, this.checkValidRange, this.checkCriterionTypeIsValid, this.checkCriterionHasValidNumberOfValues, this.checkCriterionValuesAreValid));
            case "REMOVE_DATA_VALIDATION_RULE":
                if (!this.getters.tryGetSheet(cmd.sheetId)) {
                    return "InvalidSheetId" /* CommandResult.InvalidSheetId */;
                }
                if (!this.rules[cmd.sheetId].find((rule) => rule.id === cmd.id)) {
                    return "UnknownDataValidationRule" /* CommandResult.UnknownDataValidationRule */;
                }
                break;
        }
        return "Success" /* CommandResult.Success */;
    }
    handle(cmd) {
        switch (cmd.type) {
            case "CREATE_SHEET":
                this.history.update("rules", cmd.sheetId, []);
                break;
            case "DUPLICATE_SHEET": {
                const rules = deepCopy(this.rules[cmd.sheetId]).map((rule) => ({
                    ...rule,
                    ranges: rule.ranges.map((range) => duplicateRangeInDuplicatedSheet(cmd.sheetId, cmd.sheetIdTo, range)),
                }));
                this.history.update("rules", cmd.sheetIdTo, rules);
                break;
            }
            case "DELETE_SHEET": {
                const rules = { ...this.rules };
                delete rules[cmd.sheetId];
                this.history.update("rules", rules);
                break;
            }
            case "REMOVE_DATA_VALIDATION_RULE": {
                this.removeDataValidationRule(cmd.sheetId, cmd.id);
                break;
            }
            case "ADD_DATA_VALIDATION_RULE": {
                const ranges = cmd.ranges.map((range) => this.getters.getRangeFromRangeData(range));
                this.addDataValidationRule(cmd.sheetId, { ...cmd.rule, ranges });
                break;
            }
            case "DELETE_CONTENT": {
                const zones = recomputeZones(cmd.target);
                const sheetId = cmd.sheetId;
                for (const zone of zones) {
                    for (let row = zone.top; row <= zone.bottom; row++) {
                        for (let col = zone.left; col <= zone.right; col++) {
                            const dataValidation = this.getValidationRuleForCell({ sheetId, col, row });
                            if (!dataValidation) {
                                continue;
                            }
                            if (dataValidation.criterion.type === "isBoolean" ||
                                (dataValidation.criterion.type === "isValueInList" &&
                                    !this.getters.getCell({ sheetId, col, row })?.content)) {
                                const rules = this.rules[sheetId];
                                const ranges = [this.getters.getRangeFromSheetXC(sheetId, toXC(col, row))];
                                const adaptedRules = this.removeRangesFromRules(sheetId, ranges, rules);
                                this.history.update("rules", sheetId, adaptedRules);
                            }
                        }
                    }
                }
            }
        }
    }
    getDataValidationRules(sheetId) {
        return this.rules[sheetId];
    }
    getDataValidationRule(sheetId, id) {
        return this.rules[sheetId].find((rule) => rule.id === id);
    }
    getValidationRuleForCell({ sheetId, col, row }) {
        if (!this.rules[sheetId]) {
            return undefined;
        }
        for (const rule of this.rules[sheetId]) {
            for (const range of rule.ranges) {
                if (isInside(col, row, range.zone)) {
                    return rule;
                }
            }
        }
        return undefined;
    }
    cellHasListDataValidationIcon(cellPosition) {
        const rule = this.getValidationRuleForCell(cellPosition);
        if (!rule)
            return false;
        return ((rule.criterion.type === "isValueInList" || rule.criterion.type === "isValueInRange") &&
            (rule.criterion.displayStyle === "arrow" || rule.criterion.displayStyle === "chip"));
    }
    addDataValidationRule(sheetId, newRule) {
        const rules = this.rules[sheetId];
        if (newRule.criterion.type === "isBoolean") {
            this.setCenterStyleToBooleanCells(newRule);
        }
        else if (newRule.criterion.type === "isValueInList") {
            newRule.criterion.values = Array.from(new Set(newRule.criterion.values));
        }
        const adaptedRules = this.removeRangesFromRules(sheetId, newRule.ranges, rules, newRule.id);
        const ruleIndex = adaptedRules.findIndex((rule) => rule.id === newRule.id);
        if (ruleIndex !== -1) {
            adaptedRules[ruleIndex] = newRule;
            this.history.update("rules", sheetId, adaptedRules);
        }
        else {
            this.history.update("rules", sheetId, [...adaptedRules, newRule]);
        }
    }
    removeRangesFromRules(sheetId, ranges, rules, editingRuleId) {
        rules = deepCopy(rules);
        for (const rule of rules) {
            if (rule.id === editingRuleId) {
                continue; // Skip the rule being edited to preserve its place in the list
            }
            rule.ranges = this.getters.recomputeRanges(rule.ranges, ranges);
        }
        return rules.filter((rule) => rule.ranges.length > 0);
    }
    removeDataValidationRule(sheetId, ruleId) {
        const rules = this.rules[sheetId];
        const newRules = rules.filter((rule) => rule.id !== ruleId);
        this.history.update("rules", sheetId, newRules);
    }
    setCenterStyleToBooleanCells(rule) {
        for (const position of getCellPositionsInRanges(rule.ranges)) {
            const cell = this.getters.getCell(position);
            const style = {
                ...cell?.style,
                align: cell?.style?.align ?? "center",
                verticalAlign: cell?.style?.verticalAlign ?? "middle",
            };
            this.dispatch("UPDATE_CELL", { ...position, style });
        }
    }
    checkEmptyRange(cmd) {
        return cmd.ranges.length ? "Success" /* CommandResult.Success */ : "EmptyRange" /* CommandResult.EmptyRange */;
    }
    import(data) {
        for (const sheet of data.sheets) {
            this.rules[sheet.id] = [];
            if (!sheet.dataValidationRules) {
                continue;
            }
            for (const rule of sheet.dataValidationRules) {
                this.rules[sheet.id].push({
                    ...rule,
                    ranges: rule.ranges.map((range) => this.getters.getRangeFromSheetXC(sheet.id, range)),
                });
            }
        }
    }
    export(data) {
        if (!data.sheets) {
            return;
        }
        for (const sheet of data.sheets) {
            sheet.dataValidationRules = [];
            for (const rule of this.rules[sheet.id]) {
                sheet.dataValidationRules.push({
                    ...rule,
                    ranges: rule.ranges.map((range) => this.getters.getRangeString(range, sheet.id)),
                });
            }
        }
    }
    exportForExcel(data) {
        if (!data.sheets) {
            return;
        }
        for (const sheet of data.sheets) {
            sheet.dataValidationRules = [];
            for (const rule of this.rules[sheet.id]) {
                const excelRule = {
                    ...deepCopy(rule),
                    ranges: rule.ranges.map((range) => this.getters.getRangeString(range, sheet.id, { useBoundedReference: true })),
                };
                if (rule.criterion.type === "isValueInRange") {
                    excelRule.criterion.values = rule.criterion.values.map((value) => {
                        const range = this.getters.getRangeFromSheetXC(sheet.id, value);
                        return this.getters.getRangeString(range, sheet.id, {
                            useBoundedReference: true,
                            useFixedReference: true,
                        });
                    });
                }
                sheet.dataValidationRules.push(excelRule);
            }
        }
    }
    checkCriterionTypeIsValid(cmd) {
        return criterionEvaluatorRegistry.contains(cmd.rule.criterion.type)
            ? "Success" /* CommandResult.Success */
            : "UnknownDataValidationCriterionType" /* CommandResult.UnknownDataValidationCriterionType */;
    }
    checkCriterionHasValidNumberOfValues(cmd) {
        const criterion = cmd.rule.criterion;
        const evaluator = criterionEvaluatorRegistry.get(criterion.type);
        const expectedNumberOfValues = evaluator.numberOfValues(criterion);
        if (expectedNumberOfValues !== undefined &&
            criterion.values.length !== expectedNumberOfValues) {
            return "InvalidNumberOfCriterionValues" /* CommandResult.InvalidNumberOfCriterionValues */;
        }
        return "Success" /* CommandResult.Success */;
    }
    checkCriterionValuesAreValid(cmd) {
        const criterion = cmd.rule.criterion;
        const evaluator = criterionEvaluatorRegistry.get(criterion.type);
        const isInvalid = (value) => {
            if (evaluator.allowedValues === "onlyFormulas" && !value.startsWith("=")) {
                return true;
            }
            if (value.startsWith("=")) {
                return evaluator.allowedValues === "onlyLiterals" || compile(value).isBadExpression;
            }
            return !evaluator.isCriterionValueValid(value);
        };
        if (criterion.values.some(isInvalid)) {
            return "InvalidDataValidationCriterionValue" /* CommandResult.InvalidDataValidationCriterionValue */;
        }
        return "Success" /* CommandResult.Success */;
    }
    checkValidRange(cmd) {
        const ranges = cmd.ranges.map((range) => this.getters.getRangeFromRangeData(range));
        const stringRanges = ranges.map((range) => this.getters.getRangeString(range, cmd.sheetId));
        if (stringRanges.some((xc) => !this.getters.isRangeValid(xc))) {
            return "InvalidRange" /* CommandResult.InvalidRange */;
        }
        return "Success" /* CommandResult.Success */;
    }
}

class FigurePlugin extends CorePlugin {
    static getters = ["getFigures", "getFigure", "getFigureSheetId"];
    figures = {};
    insertionOrders = []; // TODO use a list in master
    // ---------------------------------------------------------------------------
    // Command Handling
    // ---------------------------------------------------------------------------
    adaptRanges({ applyChange }, sheetId) {
        for (const figure of this.getFigures(sheetId)) {
            const change = applyChange(this.getters.getRangeFromZone(sheetId, {
                left: figure.col,
                right: figure.col,
                top: figure.row,
                bottom: figure.row,
            }));
            if (change.changeType === "MOVE") {
                this.history.update("figures", sheetId, figure.id, "col", change.range.zone.right);
                this.history.update("figures", sheetId, figure.id, "row", change.range.zone.bottom);
            }
            else if (change.changeType === "REMOVE") {
                const fullchange = applyChange(this.getters.getRangeFromZone(sheetId, {
                    left: 0,
                    right: figure.col - 1,
                    top: 0,
                    bottom: figure.row - 1,
                }));
                let { offset, col, row } = figure;
                if (fullchange.changeType !== "NONE") {
                    col = fullchange.range.zone.right + 1;
                    row = fullchange.range.zone.bottom + 1;
                }
                ({ offset, col, row } = this.getPositionInSheet(sheetId, { ...figure, col, row }));
                this.history.update("figures", sheetId, figure.id, "offset", offset);
                this.history.update("figures", sheetId, figure.id, "col", col);
                this.history.update("figures", sheetId, figure.id, "row", row);
            }
        }
    }
    allowDispatch(cmd) {
        switch (cmd.type) {
            case "CREATE_FIGURE":
                return this.checkValidations(cmd, this.checkFigureDuplicate, this.checkFigureAnchorOffset);
            case "UPDATE_FIGURE":
                return this.checkValidations(cmd, this.checkFigureExists, this.checkFigureAnchorOffset);
            case "DELETE_FIGURE":
                return this.checkFigureExists(cmd);
            default:
                return "Success" /* CommandResult.Success */;
        }
    }
    beforeHandle(cmd) {
        switch (cmd.type) {
            case "DELETE_SHEET":
                this.getters.getFigures(cmd.sheetId).forEach((figure) => {
                    this.dispatch("DELETE_FIGURE", { figureId: 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":
                const figure = {
                    id: cmd.figureId,
                    col: cmd.col,
                    row: cmd.row,
                    offset: cmd.offset,
                    width: cmd.size.width,
                    height: cmd.size.height,
                    tag: cmd.tag,
                };
                this.addFigure(figure, cmd.sheetId);
                break;
            case "UPDATE_FIGURE":
                this.updateFigure(cmd);
                break;
            case "DELETE_FIGURE":
                this.removeFigure(cmd.figureId, cmd.sheetId);
                break;
            case "REMOVE_COLUMNS_ROWS":
                if (cmd.dimension === "COL") {
                    this.onColRemove(cmd.sheetId);
                }
                else {
                    this.onRowRemove(cmd.sheetId);
                }
                break;
            case "DUPLICATE_SHEET": {
                for (const figure of this.getFigures(cmd.sheetId)) {
                    const figureId = figure.id;
                    const fig = this.figures[cmd.sheetId]?.[figureId];
                    if (!fig) {
                        continue;
                    }
                    const figureIdBase = figureId.split(FIGURE_ID_SPLITTER).pop();
                    const duplicatedFigureId = `${cmd.sheetIdTo}${FIGURE_ID_SPLITTER}${figureIdBase}`;
                    this.dispatch("CREATE_FIGURE", {
                        figureId: duplicatedFigureId,
                        ...fig,
                        size: { width: fig.width, height: fig.height },
                        sheetId: cmd.sheetIdTo,
                    });
                }
                break;
            }
        }
    }
    onColRemove(sheetId) {
        const numHeader = this.getters.getNumberCols(sheetId);
        const remainingSize = new Array(numHeader + 1);
        remainingSize[numHeader] = 0;
        for (let i = numHeader - 1; i >= 0; i--) {
            remainingSize[i] = remainingSize[i + 1] + this.getters.getColSize(sheetId, i);
        }
        for (const figure of this.getFigures(sheetId)) {
            if (figure.offset.x + figure.width > remainingSize[figure.col]) {
                let x = figure.offset.x;
                let col = figure.col;
                x = Math.min(x, remainingSize[col] - figure.width);
                while (x < 0 && col > 0) {
                    col--;
                    x = remainingSize[col] - figure.width;
                }
                if (x !== figure.offset.x) {
                    this.history.update("figures", sheetId, figure.id, "offset", {
                        x: Math.max(x, 0),
                        y: figure.offset.y,
                    });
                }
                if (col !== figure.col) {
                    this.history.update("figures", sheetId, figure.id, "col", col);
                }
            }
        }
    }
    onRowRemove(sheetId) {
        const numHeader = this.getters.getNumberRows(sheetId);
        const remainingSize = new Array(numHeader + 1);
        remainingSize[numHeader] = 0;
        for (let i = numHeader - 1; i >= 0; i--) {
            // TODO : since the row size is an UI value now, this doesn't work anymore. Using the default cell height is
            // a temporary solution at best, but is broken.
            remainingSize[i] =
                remainingSize[i + 1] + (this.getters.getUserRowSize(sheetId, i) ?? DEFAULT_CELL_HEIGHT);
        }
        for (const figure of this.getFigures(sheetId)) {
            if (figure.offset.y + figure.height > remainingSize[figure.row]) {
                let y = figure.offset.y;
                let row = figure.row;
                for (let row_size = this.getters.getUserRowSize(sheetId, row) ?? DEFAULT_CELL_HEIGHT; row_size < y; row_size = this.getters.getUserRowSize(sheetId, row) ?? DEFAULT_CELL_HEIGHT) {
                    row += 1;
                    y -= row_size;
                }
                y = Math.min(y, remainingSize[row] - figure.height);
                while (y < 0 && row > 0) {
                    row--;
                    y = remainingSize[row] - figure.height;
                }
                if (y !== figure.offset.y) {
                    this.history.update("figures", sheetId, figure.id, "offset", {
                        x: figure.offset.x,
                        y: Math.max(0, y),
                    });
                }
                if (row !== figure.row) {
                    this.history.update("figures", sheetId, figure.id, "row", row);
                }
            }
            else if (figure.offset.y + figure.height > remainingSize[0]) {
                this.history.update("figures", sheetId, figure.id, "offset", {
                    x: figure.offset.x,
                    y: Math.max(remainingSize[0] - figure.height, 0),
                });
            }
        }
    }
    getPositionInSheet(sheetId, figure) {
        const { numberOfRows, numberOfCols } = this.getters.getSheetSize(sheetId);
        let availableHeight = 0, availableWidth = 0;
        let rowNum, colNum;
        let { col, row } = figure;
        const offset = { ...figure.offset };
        // Check figure is inside the sheet vertical boundaries
        // This can be wrong if the offset of the figure is greater than the size of it's anchor cell
        for (rowNum = numberOfRows; availableHeight < figure.height && rowNum > 0; rowNum--) {
            availableHeight += this.getters.getUserRowSize(sheetId, rowNum - 1) ?? DEFAULT_CELL_HEIGHT;
        }
        if (row > rowNum) {
            row = rowNum;
            offset.y = Math.max(availableHeight - figure.height, 0);
        }
        else if (row === rowNum) {
            offset.y = clip(offset.y, 0, Math.max(availableHeight - figure.height, 0));
        }
        // Check figure is inside horinzontal sheet boundaries
        // This can be wrong if the offset of the figure is greater than the size of it's anchor cell
        for (colNum = numberOfCols; availableWidth < figure.width && colNum > 0; colNum--) {
            availableWidth += this.getters.getColSize(sheetId, colNum - 1);
        }
        if (col > colNum) {
            col = colNum;
            offset.x = Math.max(availableWidth - figure.width, 0);
        }
        else if (colNum === col) {
            offset.x = clip(offset.x, 0, Math.max(availableWidth - figure.width, 0));
        }
        return { col, row, offset };
    }
    updateFigure(cmd) {
        if (!("figureId" in cmd) || !("sheetId" in cmd)) {
            return;
        }
        const { figureId, sheetId, ...update } = cmd;
        const figure = { ...this.getFigure(sheetId, figureId), ...update };
        for (const [key, value] of Object.entries(update)) {
            switch (key) {
                case "tag":
                    this.history.update("figures", sheetId, figure.id, key, value);
                    break;
                case "offset":
                    this.history.update("figures", sheetId, figure.id, key, value);
                    break;
                case "col":
                case "row":
                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);
        this.history.update("insertionOrders", this.insertionOrders.length, figure.id);
    }
    deleteSheet(sheetId) {
        this.history.update("insertionOrders", this.insertionOrders.filter((id) => !this.figures[sheetId]?.[id]));
        this.history.update("figures", sheetId, undefined);
    }
    removeFigure(id, sheetId) {
        this.history.update("insertionOrders", this.insertionOrders.filter((figureId) => figureId !== id));
        this.history.update("figures", sheetId, id, undefined);
    }
    checkFigureExists(cmd) {
        if (this.figures[cmd.sheetId]?.[cmd.figureId] === undefined) {
            return "FigureDoesNotExist" /* CommandResult.FigureDoesNotExist */;
        }
        return "Success" /* CommandResult.Success */;
    }
    checkFigureDuplicate(cmd) {
        if (Object.values(this.figures).find((sheet) => sheet?.[cmd.figureId])) {
            return "DuplicatedFigureId" /* CommandResult.DuplicatedFigureId */;
        }
        return "Success" /* CommandResult.Success */;
    }
    checkFigureAnchorOffset(cmd) {
        if (cmd.col < 0 || cmd.row < 0 || (cmd.offset && (cmd.offset.x < 0 || cmd.offset.y < 0))) {
            return "WrongSheetPosition" /* CommandResult.WrongSheetPosition */;
        }
        return "Success" /* CommandResult.Success */;
    }
    // ---------------------------------------------------------------------------
    // Getters
    // ---------------------------------------------------------------------------
    getFigures(sheetId) {
        const figures = [];
        for (const figureId of this.insertionOrders) {
            const figure = this.figures[sheetId]?.[figureId];
            if (figure) {
                figures.push(figure);
            }
        }
        return figures;
    }
    getFigure(sheetId, figureId) {
        return this.figures[sheetId]?.[figureId];
    }
    getFigureSheetId(figureId) {
        return Object.keys(this.figures).find((sheetId) => this.figures[sheetId]?.[figureId] !== undefined);
    }
    // ---------------------------------------------------------------------------
    // Import/Export
    // ---------------------------------------------------------------------------
    import(data) {
        for (const sheet of data.sheets) {
            for (const figure of sheet.figures) {
                this.addFigure(figure, sheet.id);
            }
        }
    }
    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) {
        return this.export(data);
    }
}

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

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

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

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

class RangeAdapterPlugin {
    getters;
    providers = [];
    isAdaptingRanges = false;
    constructor(getters) {
        this.getters = getters;
    }
    static getters = [
        "copyFormulaStringForSheet",
        "extendRange",
        "getRangeString",
        "getRangeFromSheetXC",
        "createAdaptedRanges",
        "getRangeData",
        "getRangeDataFromXc",
        "getRangeDataFromZone",
        "getRangeFromRangeData",
        "getRangeFromZone",
        "getRangesUnion",
        "recomputeRanges",
        "isRangeValid",
        "removeRangesSheetPrefix",
    ];
    // ---------------------------------------------------------------------------
    // Command Handling
    // ---------------------------------------------------------------------------
    allowDispatch(cmd) {
        if (cmd.type === "MOVE_RANGES") {
            return cmd.target.length === 1 ? "Success" /* CommandResult.Success */ : "InvalidZones" /* CommandResult.InvalidZones */;
        }
        return "Success" /* CommandResult.Success */;
    }
    beforeHandle(command) { }
    handle(cmd) {
        if (this.isAdaptingRanges) {
            throw new Error("Plugins cannot dispatch commands during adaptRanges phase");
        }
        const rangeAdapter = getRangeAdapter(cmd);
        if (rangeAdapter) {
            this.executeOnAllRanges(rangeAdapter);
        }
    }
    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;
        };
    }
    executeOnAllRanges(rangeAdapter) {
        this.isAdaptingRanges = true;
        const adapterFunctions = {
            applyChange: this.verifyRangeRemoved(rangeAdapter.applyChange),
            adaptRangeString: (defaultSheetId, sheetXC) => adaptStringRange(defaultSheetId, sheetXC, rangeAdapter),
            adaptFormulaString: (defaultSheetId, formula) => adaptFormulaStringRanges(defaultSheetId, formula, rangeAdapter),
        };
        for (const provider of this.providers) {
            provider(adapterFunctions, rangeAdapter.sheetId, rangeAdapter.sheetName);
        }
        this.isAdaptingRanges = false;
    }
    /**
     * 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) {
        return ranges.map((range) => {
            if (!isZoneValid(range.zone)) {
                return range;
            }
            const copySheetId = range.prefixSheet ? range.sheetId : sheetId;
            const isFullRow = isFullRowRange(range);
            const isFullCol = isFullColRange(range);
            const unboundZone = {
                ...range.unboundedZone,
                // Don't shift left if the range is a full row without header
                left: 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: 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: 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: isFullCol
                    ? range.unboundedZone.bottom
                    : range.unboundedZone.bottom +
                        ((range.parts[1] || range.parts[0]).rowFixed ? 0 : offsetY),
            };
            return orderRange(createRange({ ...range, sheetId: copySheetId, zone: unboundZone }, this.getters.getSheetSize));
        });
    }
    /**
     * Remove the sheet name prefix if a range is part of the given sheet.
     */
    removeRangesSheetPrefix(sheetId, ranges) {
        return ranges.map((range) => {
            if (range.prefixSheet && range.sheetId === sheetId) {
                return { ...range, prefixSheet: false };
            }
            return range;
        });
    }
    extendRange(range, dimension, quantity) {
        const right = dimension === "COL" ? range.zone.right + quantity : range.zone.right;
        const bottom = dimension === "ROW" ? range.zone.bottom + quantity : range.zone.bottom;
        const unboundedZone = {
            left: range.zone.left,
            top: range.zone.top,
            right: isFullRowRange(range) ? undefined : right,
            bottom: isFullColRange(range) ? undefined : bottom,
        };
        return createRange({ ...range, zone: unboundedZone }, this.getters.getSheetSize);
    }
    /**
     * 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 createInvalidRange(sheetXC);
        }
        const { sheetName } = splitReference(sheetXC);
        const sheetId = this.getters.getSheetIdByName(sheetName) || defaultSheetId;
        const invalidSheetName = sheetName && !this.getters.getSheetIdByName(sheetName) ? sheetName : undefined;
        return createRangeFromXc({ xc: sheetXC, sheetId, invalidSheetName }, this.getters.getSheetSize);
    }
    /**
     * Gets the string that represents the range as it is at the moment of the call.
     * The string will be prefixed with the sheet name if the call specified a sheet id in `forSheetId`
     * different than the sheet on which the range has been created.
     *
     * @param range the range (received from getRangeFromXC or getRangeFromZone)
     * @param forSheetId the id of the sheet where the range string is supposed to be used.
     * @param options
     * @param options.useBoundedReference if true, the range will be returned with bounded row and column
     * @param options.useFixedReference if true, the range will be returned with fixed row and column
     */
    getRangeString(range, forSheetId, options = { useBoundedReference: false, useFixedReference: false }) {
        if (!range) {
            return CellErrorType.InvalidReference;
        }
        if (range.invalidXc) {
            return range.invalidXc;
        }
        if (!this.getters.tryGetSheet(range.sheetId)) {
            return CellErrorType.InvalidReference;
        }
        return getRangeString(range, forSheetId, this.getters.getSheetName, options);
    }
    getRangeDataFromXc(sheetId, xc) {
        const range = this.getters.getRangeFromSheetXC(sheetId, xc);
        return this.getRangeDataFromZone(range.sheetId, range.unboundedZone);
    }
    getRangeDataFromZone(sheetId, zone) {
        zone = this.getters.getUnboundedZone(sheetId, zone);
        return { _sheetId: sheetId, _zone: zone };
    }
    getRangeData(range) {
        return this.getRangeDataFromZone(range.sheetId, range.unboundedZone);
    }
    getRangeFromZone(sheetId, zone) {
        return createRange({
            sheetId,
            zone,
            parts: [
                { colFixed: false, rowFixed: false },
                { colFixed: false, rowFixed: false },
            ],
            prefixSheet: false,
        }, this.getters.getSheetSize);
    }
    /**
     * Allows you to recompute ranges from the same sheet
     */
    recomputeRanges(ranges, rangesToRemove) {
        const zones = ranges.map((range) => range.unboundedZone);
        const zonesToRemove = rangesToRemove.map((range) => range.unboundedZone);
        return recomputeZones(zones, zonesToRemove).map((zone) => this.getRangeFromZone(ranges[0].sheetId, zone));
    }
    getRangeFromRangeData(data) {
        if (!this.getters.tryGetSheet(data._sheetId)) {
            return createInvalidRange(CellErrorType.InvalidReference);
        }
        const rangeInterface = {
            prefixSheet: false,
            zone: data._zone,
            sheetId: data._sheetId,
            invalidSheetName: undefined,
            parts: [
                { colFixed: false, rowFixed: false },
                { colFixed: false, rowFixed: false },
            ],
        };
        return createRange(rangeInterface, this.getters.getSheetSize);
    }
    isRangeValid(rangeStr) {
        if (!rangeStr) {
            return false;
        }
        const { xc, sheetName } = splitReference(rangeStr);
        return (xc.match(rangeReference) !== null &&
            (!sheetName || this.getters.getSheetIdByName(sheetName) !== undefined));
    }
    getRangesUnion(ranges) {
        const zones = ranges.map((range) => range.unboundedZone);
        const unionOfZones = unionUnboundedZones(...zones);
        return this.getRangeFromZone(ranges[0].sheetId, unionOfZones);
    }
    /**
     * Copy a formula string to another sheet.
     *
     * @param mode
     * `keepSameReference` will make the formula reference the exact same ranges,
     * `moveReference` will change all the references to `sheetIdFrom` into references to `sheetIdTo`.
     */
    copyFormulaStringForSheet(sheetIdFrom, sheetIdTo, formula, mode) {
        if (!formula.startsWith("=")) {
            return formula;
        }
        const compiledFormula = compile(formula);
        const updatedDependencies = compiledFormula.dependencies.map((dep) => {
            const range = this.getters.getRangeFromSheetXC(sheetIdFrom, dep);
            return mode === "keepSameReference"
                ? range
                : duplicateRangeInDuplicatedSheet(sheetIdFrom, sheetIdTo, range);
        });
        return this.getters.getFormulaString(sheetIdTo, compiledFormula.tokens, updatedDependencies);
    }
}

class SheetPlugin extends CorePlugin {
    static getters = [
        "getSheetName",
        "tryGetSheetName",
        "getSheet",
        "tryGetSheet",
        "getSheetIdByName",
        "getSheetIds",
        "getVisibleSheetIds",
        "isSheetVisible",
        "doesHeaderExist",
        "doesHeadersExist",
        "getCell",
        "getCellPosition",
        "getColsZone",
        "getRowCells",
        "getRowsZone",
        "getNumberCols",
        "getNumberRows",
        "getNumberHeaders",
        "getGridLinesVisibility",
        "getNextSheetName",
        "getSheetSize",
        "getSheetZone",
        "getPaneDivisions",
        "checkZonesExistInSheet",
        "getCommandZones",
        "getUnboundedZone",
        "checkElementsIncludeAllNonFrozenHeaders",
        "getDuplicateSheetName",
    ];
    sheetIdsMapName = {};
    orderedSheetIds = [];
    sheets = {};
    cellPosition = {};
    // ---------------------------------------------------------------------------
    // Command Handling
    // ---------------------------------------------------------------------------
    allowDispatch(cmd) {
        const genericChecks = this.chainValidations(this.checkSheetExists, this.checkZonesAreInSheet)(cmd);
        if (genericChecks !== "Success" /* CommandResult.Success */) {
            return genericChecks;
        }
        switch (cmd.type) {
            case "HIDE_SHEET": {
                if (this.getVisibleSheetIds().length === 1) {
                    return "NotEnoughSheets" /* CommandResult.NotEnoughSheets */;
                }
                return "Success" /* CommandResult.Success */;
            }
            case "CREATE_SHEET": {
                return this.checkValidations(cmd, this.checkSheetName, this.checkSheetPosition);
            }
            case "DUPLICATE_SHEET": {
                if (this.sheets[cmd.sheetIdTo])
                    return "DuplicatedSheetId" /* CommandResult.DuplicatedSheetId */;
                if (this.orderedSheetIds.map(this.getSheetName.bind(this)).includes(cmd.sheetNameTo))
                    return "DuplicatedSheetName" /* CommandResult.DuplicatedSheetName */;
                return "Success" /* CommandResult.Success */;
            }
            case "MOVE_SHEET":
                try {
                    const currentIndex = this.orderedSheetIds.findIndex((id) => id === cmd.sheetId);
                    this.findIndexOfTargetSheet(currentIndex, cmd.delta);
                    return "Success" /* CommandResult.Success */;
                }
                catch (e) {
                    return "WrongSheetMove" /* CommandResult.WrongSheetMove */;
                }
            case "RENAME_SHEET":
                return this.isRenameAllowed(cmd);
            case "COLOR_SHEET":
                return !cmd.color || isColorValid(cmd.color)
                    ? "Success" /* CommandResult.Success */
                    : "InvalidColor" /* CommandResult.InvalidColor */;
            case "DELETE_SHEET":
                return this.getVisibleSheetIds().length > 1
                    ? "Success" /* CommandResult.Success */
                    : "NotEnoughSheets" /* CommandResult.NotEnoughSheets */;
            case "ADD_COLUMNS_ROWS":
                if (!this.doesHeaderExist(cmd.sheetId, cmd.dimension, cmd.base)) {
                    return "InvalidHeaderIndex" /* CommandResult.InvalidHeaderIndex */;
                }
                else if (cmd.quantity <= 0) {
                    return "InvalidQuantity" /* CommandResult.InvalidQuantity */;
                }
                return "Success" /* CommandResult.Success */;
            case "REMOVE_COLUMNS_ROWS": {
                const min = largeMin(cmd.elements);
                const max = largeMax(cmd.elements);
                if (min < 0 || !this.doesHeaderExist(cmd.sheetId, cmd.dimension, max)) {
                    return "InvalidHeaderIndex" /* CommandResult.InvalidHeaderIndex */;
                }
                else if (this.checkElementsIncludeAllNonFrozenHeaders(cmd.sheetId, cmd.dimension, cmd.elements)) {
                    return "NotEnoughElements" /* CommandResult.NotEnoughElements */;
                }
                else {
                    return "Success" /* CommandResult.Success */;
                }
            }
            case "FREEZE_ROWS": {
                return this.checkValidations(cmd, this.checkRowFreezeQuantity, this.checkRowFreezeOverlapMerge);
            }
            case "FREEZE_COLUMNS": {
                return this.checkValidations(cmd, this.checkColFreezeQuantity, this.checkColFreezeOverlapMerge);
            }
            default:
                return "Success" /* CommandResult.Success */;
        }
    }
    handle(cmd) {
        switch (cmd.type) {
            case "SET_GRID_LINES_VISIBILITY":
                this.setGridLinesVisibility(cmd.sheetId, cmd.areGridLinesVisible);
                break;
            case "CREATE_SHEET":
                const sheet = this.createSheet(cmd.sheetId, cmd.name || this.getNextSheetName(), cmd.cols || 26, cmd.rows || 100, cmd.position);
                this.history.update("sheetIdsMapName", toStandardizedSheetName(sheet.name), sheet.id);
                break;
            case "MOVE_SHEET":
                this.moveSheet(cmd.sheetId, cmd.delta);
                break;
            case "RENAME_SHEET":
                this.renameSheet(this.sheets[cmd.sheetId], cmd.newName);
                break;
            case "COLOR_SHEET":
                this.history.update("sheets", cmd.sheetId, "color", cmd.color);
                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) {
        // 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 (const sheet of data.sheets) {
            this.sheetIdsMapName[toStandardizedSheetName(sheet.name)] = sheet.id;
        }
        for (const sheetData of data.sheets) {
            const name = sheetData.name || "Sheet" + (Object.keys(this.sheets).length + 1);
            const { colNumber, rowNumber } = this.getImportedSheetSize(sheetData);
            const sheet = {
                id: sheetData.id,
                name: name,
                numberOfCols: colNumber,
                rows: createDefaultRows(rowNumber),
                areGridLinesVisible: sheetData.areGridLinesVisible === undefined ? true : sheetData.areGridLinesVisible,
                isVisible: sheetData.isVisible,
                panes: {
                    xSplit: sheetData.panes?.xSplit || 0,
                    ySplit: sheetData.panes?.ySplit || 0,
                },
                color: sheetData.color,
            };
            this.orderedSheetIds.push(sheet.id);
            this.sheets[sheet.id] = sheet;
        }
    }
    exportSheets(data) {
        data.sheets = this.orderedSheetIds.filter(isDefined).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: {},
                styles: {},
                formats: {},
                borders: {},
                conditionalFormats: [],
                dataValidationRules: [],
                figures: [],
                tables: [],
                areGridLinesVisible: sheet.areGridLinesVisible === undefined ? true : sheet.areGridLinesVisible,
                isVisible: sheet.isVisible,
                color: sheet.color,
            };
            if (sheet.panes.xSplit || sheet.panes.ySplit) {
                sheetData.panes = sheet.panes;
            }
            return sheetData;
        });
    }
    export(data) {
        this.exportSheets(data);
    }
    exportForExcel(data) {
        this.exportSheets(data);
    }
    // ---------------------------------------------------------------------------
    // Getters
    // ---------------------------------------------------------------------------
    getGridLinesVisibility(sheetId) {
        return this.getSheet(sheetId).areGridLinesVisible;
    }
    tryGetSheet(sheetId) {
        return this.sheets[sheetId];
    }
    getSheet(sheetId) {
        const sheet = this.sheets[sheetId];
        if (!sheet) {
            throw new Error(`Sheet ${sheetId} not found.`);
        }
        return sheet;
    }
    isSheetVisible(sheetId) {
        return this.getSheet(sheetId).isVisible;
    }
    /**
     * Return the sheet name. Throw if the sheet is not found.
     */
    getSheetName(sheetId) {
        return this.getSheet(sheetId).name;
    }
    /**
     * Return the sheet name or undefined if the sheet doesn't exist.
     */
    tryGetSheetName(sheetId) {
        return this.tryGetSheet(sheetId)?.name;
    }
    getSheetIdByName(name) {
        if (name) {
            return this.sheetIdsMapName[toStandardizedSheetName(name)];
        }
        return undefined;
    }
    getSheetIds() {
        return this.orderedSheetIds;
    }
    getVisibleSheetIds() {
        return this.orderedSheetIds.filter(this.isSheetVisible.bind(this));
    }
    doesHeaderExist(sheetId, dimension, index) {
        return dimension === "COL"
            ? index >= 0 && index < this.getNumberCols(sheetId)
            : index >= 0 && index < this.getNumberRows(sheetId);
    }
    doesHeadersExist(sheetId, dimension, headerIndexes) {
        return headerIndexes.every((index) => this.doesHeaderExist(sheetId, dimension, index));
    }
    getCell({ sheetId, col, row }) {
        const sheet = this.tryGetSheet(sheetId);
        const cellId = sheet?.rows[row]?.cells[col];
        if (cellId === undefined) {
            return undefined;
        }
        return this.getters.getCellById(cellId);
    }
    getColsZone(sheetId, start, end) {
        return {
            top: 0,
            bottom: this.getNumberRows(sheetId) - 1,
            left: start,
            right: end,
        };
    }
    getRowCells(sheetId, row) {
        return Object.values(this.getSheet(sheetId).rows[row]?.cells).filter(isDefined);
    }
    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 {
            numberOfRows: this.getNumberRows(sheetId),
            numberOfCols: this.getNumberCols(sheetId),
        };
    }
    getSheetZone(sheetId) {
        return {
            top: 0,
            left: 0,
            bottom: this.getNumberRows(sheetId) - 1,
            right: this.getNumberCols(sheetId) - 1,
        };
    }
    getUnboundedZone(sheetId, zone) {
        if (zone.bottom === undefined || zone.right === undefined) {
            return zone;
        }
        const isFullRow = zone.left === 0 && zone.right === this.getNumberCols(sheetId) - 1;
        const isFullCol = zone.top === 0 && zone.bottom === this.getNumberRows(sheetId) - 1;
        return {
            ...zone,
            bottom: isFullCol ? undefined : zone.bottom,
            // cannot be unbounded in the 2 dimensions at once
            right: isFullRow && !isFullCol ? undefined : zone.right,
        };
    }
    getPaneDivisions(sheetId) {
        return this.getSheet(sheetId).panes;
    }
    setPaneDivisions(sheetId, base, dimension) {
        const panes = { ...this.getPaneDivisions(sheetId) };
        if (dimension === "COL") {
            panes.xSplit = base;
        }
        else if (dimension === "ROW") {
            panes.ySplit = base;
        }
        this.history.update("sheets", sheetId, "panes", panes);
    }
    /**
     * Checks if all non-frozen header indices are present in the provided elements of selected rows/columns.
     * This validation ensures that all rows or columns cannot be deleted when frozen panes exist.
     */
    checkElementsIncludeAllNonFrozenHeaders(sheetId, dimension, elements) {
        const paneDivisions = this.getters.getPaneDivisions(sheetId);
        const startIndex = dimension === "ROW" ? paneDivisions.ySplit : paneDivisions.xSplit;
        const endIndex = this.getters.getNumberHeaders(sheetId, dimension);
        if (!startIndex) {
            return false;
        }
        const indicesToCheck = range(startIndex, endIndex);
        return includesAll(elements, indicesToCheck);
    }
    // ---------------------------------------------------------------------------
    // Row/Col manipulation
    // ---------------------------------------------------------------------------
    getCommandZones(cmd) {
        const zones = [];
        if ("zone" in cmd) {
            zones.push(cmd.zone);
        }
        if ("target" in cmd) {
            zones.push(...cmd.target);
        }
        if ("ranges" in cmd) {
            zones.push(...cmd.ranges.map((rangeData) => this.getters.getRangeFromRangeData(rangeData).zone));
        }
        if ("col" in cmd && cmd.col !== undefined && "row" in cmd && cmd.row !== undefined) {
            zones.push({ top: cmd.row, left: cmd.col, bottom: cmd.row, right: cmd.col });
        }
        return zones;
    }
    /**
     * Check if zones in the command are well formed and
     * not outside the sheet.
     */
    checkZonesExistInSheet(sheetId, zones) {
        if (!zones.every(isZoneValid))
            return "InvalidRange" /* CommandResult.InvalidRange */;
        if (zones.length) {
            const sheetZone = this.getSheetZone(sheetId);
            return zones.every((zone) => isZoneInside(zone, sheetZone))
                ? "Success" /* CommandResult.Success */
                : "TargetOutOfSheet" /* CommandResult.TargetOutOfSheet */;
        }
        return "Success" /* CommandResult.Success */;
    }
    updateCellPosition(cmd) {
        const { sheetId, cellId, col, row } = cmd;
        if (cellId) {
            this.setNewPosition(cellId, sheetId, col, row);
        }
        else {
            this.clearPosition(sheetId, col, row);
        }
    }
    /**
     * Set the cell at a new position and clear its previous position.
     */
    setNewPosition(cellId, sheetId, col, row) {
        const currentPosition = this.cellPosition[cellId];
        if (currentPosition) {
            this.clearPosition(sheetId, currentPosition.col, currentPosition.row);
        }
        this.history.update("cellPosition", cellId, {
            row: row,
            col: col,
            sheetId: sheetId,
        });
        this.history.update("sheets", sheetId, "rows", row, "cells", col, cellId);
    }
    /**
     * Remove the cell at the given position (if there's one)
     */
    clearPosition(sheetId, col, row) {
        const cellId = this.sheets[sheetId]?.rows[row].cells[col];
        if (cellId) {
            this.history.update("cellPosition", cellId, undefined);
            this.history.update("sheets", sheetId, "rows", row, "cells", col, undefined);
        }
    }
    setGridLinesVisibility(sheetId, areGridLinesVisible) {
        this.history.update("sheets", sheetId, "areGridLinesVisible", areGridLinesVisible);
    }
    createSheet(id, name, colNumber, rowNumber, position) {
        const sheet = {
            id,
            name,
            numberOfCols: colNumber,
            rows: createDefaultRows(rowNumber),
            areGridLinesVisible: true,
            isVisible: true,
            panes: {
                xSplit: 0,
                ySplit: 0,
            },
        };
        const orderedSheetIds = this.orderedSheetIds.slice();
        orderedSheetIds.splice(position, 0, sheet.id);
        const sheets = this.sheets;
        this.history.update("orderedSheetIds", orderedSheetIds);
        this.history.update("sheets", Object.assign({}, sheets, { [sheet.id]: sheet }));
        return sheet;
    }
    moveSheet(sheetId, delta) {
        const orderedSheetIds = this.orderedSheetIds.slice();
        const currentIndex = orderedSheetIds.findIndex((id) => id === sheetId);
        const sheet = orderedSheetIds.splice(currentIndex, 1);
        const index = this.findIndexOfTargetSheet(currentIndex, delta);
        orderedSheetIds.splice(index, 0, sheet[0]);
        this.history.update("orderedSheetIds", orderedSheetIds);
    }
    findIndexOfTargetSheet(currentIndex, deltaIndex) {
        while (deltaIndex !== 0 && 0 <= currentIndex && currentIndex <= this.orderedSheetIds.length) {
            if (deltaIndex > 0) {
                currentIndex++;
                if (this.isSheetVisible(this.orderedSheetIds[currentIndex])) {
                    deltaIndex--;
                }
            }
            else if (deltaIndex < 0) {
                currentIndex--;
                if (this.isSheetVisible(this.orderedSheetIds[currentIndex])) {
                    deltaIndex++;
                }
            }
        }
        if (deltaIndex === 0) {
            return currentIndex;
        }
        throw new Error("There is not enough visible sheets");
    }
    checkSheetName(cmd) {
        const originalSheetName = this.getters.tryGetSheetName(cmd.sheetId);
        const sheetName = cmd.type === "RENAME_SHEET" ? cmd.newName : cmd.name;
        if (originalSheetName !== undefined && sheetName === originalSheetName) {
            return "UnchangedSheetName" /* CommandResult.UnchangedSheetName */;
        }
        const { orderedSheetIds, sheets } = this;
        const name = sheetName && sheetName.trim().toLowerCase();
        if (orderedSheetIds.find((id) => isSheetNameEqual(sheets[id]?.name, name) && id !== cmd.sheetId)) {
            return "DuplicatedSheetName" /* CommandResult.DuplicatedSheetName */;
        }
        if (FORBIDDEN_SHEETNAME_CHARS_IN_EXCEL_REGEX.test(name)) {
            return "ForbiddenCharactersInSheetName" /* CommandResult.ForbiddenCharactersInSheetName */;
        }
        return "Success" /* CommandResult.Success */;
    }
    checkSheetPosition(cmd) {
        const { orderedSheetIds } = this;
        if (cmd.position > orderedSheetIds.length || cmd.position < 0) {
            return "WrongSheetPosition" /* CommandResult.WrongSheetPosition */;
        }
        return "Success" /* CommandResult.Success */;
    }
    checkRowFreezeQuantity(cmd) {
        return cmd.quantity >= 1 && cmd.quantity < this.getNumberRows(cmd.sheetId)
            ? "Success" /* CommandResult.Success */
            : "InvalidFreezeQuantity" /* CommandResult.InvalidFreezeQuantity */;
    }
    checkColFreezeQuantity(cmd) {
        return cmd.quantity >= 1 && cmd.quantity < this.getNumberCols(cmd.sheetId)
            ? "Success" /* CommandResult.Success */
            : "InvalidFreezeQuantity" /* CommandResult.InvalidFreezeQuantity */;
    }
    checkRowFreezeOverlapMerge(cmd) {
        const merges = this.getters.getMerges(cmd.sheetId);
        for (const merge of merges) {
            if (merge.top < cmd.quantity && cmd.quantity <= merge.bottom) {
                return "MergeOverlap" /* CommandResult.MergeOverlap */;
            }
        }
        return "Success" /* CommandResult.Success */;
    }
    checkColFreezeOverlapMerge(cmd) {
        const merges = this.getters.getMerges(cmd.sheetId);
        for (const merge of merges) {
            if (merge.left < cmd.quantity && cmd.quantity <= merge.right) {
                return "MergeOverlap" /* CommandResult.MergeOverlap */;
            }
        }
        return "Success" /* CommandResult.Success */;
    }
    isRenameAllowed(cmd) {
        const name = cmd.newName && cmd.newName.trim().toLowerCase();
        if (!name) {
            return "MissingSheetName" /* CommandResult.MissingSheetName */;
        }
        return this.checkSheetName(cmd);
    }
    renameSheet(sheet, name) {
        const oldName = sheet.name;
        this.history.update("sheets", sheet.id, "name", name.trim());
        const sheetIdsMapName = Object.assign({}, this.sheetIdsMapName);
        delete sheetIdsMapName[toStandardizedSheetName(oldName)];
        sheetIdsMapName[toStandardizedSheetName(name)] = sheet.id;
        this.history.update("sheetIdsMapName", sheetIdsMapName);
    }
    hideSheet(sheetId) {
        this.history.update("sheets", sheetId, "isVisible", false);
    }
    showSheet(sheetId) {
        this.history.update("sheets", sheetId, "isVisible", true);
    }
    duplicateSheet(fromId, toId, 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[toStandardizedSheetName(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[toStandardizedSheetName(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 (const 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 (const 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) {
        this.dispatch("CLEAR_CELLS", {
            sheetId: sheet.id,
            target: [
                {
                    left: deletedColumn,
                    top: 0,
                    right: deletedColumn,
                    bottom: sheet.rows.length - 1,
                },
            ],
        });
        for (let rowIndex = 0; rowIndex < sheet.rows.length; rowIndex++) {
            const row = sheet.rows[rowIndex];
            for (const i in row.cells) {
                const colIndex = Number(i);
                const cellId = row.cells[i];
                if (cellId) {
                    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 (const 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 (const 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) {
        this.dispatch("CLEAR_CELLS", {
            sheetId: sheet.id,
            target: [
                {
                    left: 0,
                    top: deleteFromRow,
                    right: this.getters.getNumberCols(sheet.id),
                    bottom: deleteToRow,
                },
            ],
        });
        const numberRows = deleteToRow - deleteFromRow + 1;
        for (let rowIndex = 0; rowIndex < sheet.rows.length; rowIndex++) {
            const row = sheet.rows[rowIndex];
            if (rowIndex > deleteToRow) {
                for (const 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 (const 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 (const { col, row } of positions) {
            rowNumber = Math.max(rowNumber, row + 1);
            colNumber = Math.max(colNumber, col + 1);
        }
        return { rowNumber, colNumber };
    }
    /**
     * Check that any "sheetId" in the command matches an existing
     * sheet.
     */
    checkSheetExists(cmd) {
        if (cmd.type !== "CREATE_SHEET" && "sheetId" in cmd && this.sheets[cmd.sheetId] === undefined) {
            return "InvalidSheetId" /* CommandResult.InvalidSheetId */;
        }
        else if (cmd.type === "CREATE_SHEET" && this.sheets[cmd.sheetId] !== undefined) {
            return "DuplicatedSheetId" /* CommandResult.DuplicatedSheetId */;
        }
        return "Success" /* CommandResult.Success */;
    }
    /**
     * Check if zones in the command are well formed and
     * not outside the sheet.
     */
    checkZonesAreInSheet(cmd) {
        if (!("sheetId" in cmd))
            return "Success" /* CommandResult.Success */;
        if ("ranges" in cmd &&
            cmd.ranges.some((rangeData) => rangeData._sheetId !== "" && !this.getters.tryGetSheet(rangeData._sheetId))) {
            return "InvalidSheetId" /* CommandResult.InvalidSheetId */;
        }
        return this.checkZonesExistInSheet(cmd.sheetId, this.getCommandZones(cmd));
    }
}

class TablePlugin extends CorePlugin {
    static getters = ["getCoreTable", "getCoreTables", "getCoreTableMatchingTopLeft"];
    tables = {};
    nextTableId = 1;
    adaptRanges({ applyChange }, sheetId) {
        for (const table of this.getCoreTables(sheetId)) {
            this.applyRangeChangeOnTable(sheetId, table, applyChange);
        }
    }
    allowDispatch(cmd) {
        switch (cmd.type) {
            case "CREATE_TABLE":
                if (cmd.ranges.some((rangeData) => !this.getters.tryGetSheet(rangeData._sheetId) || rangeData._sheetId !== cmd.sheetId)) {
                    return "InvalidSheetId" /* CommandResult.InvalidSheetId */;
                }
                const zones = cmd.ranges.map((rangeData) => this.getters.getRangeFromRangeData(rangeData).zone);
                if (!areZonesContinuous(zones)) {
                    return "NonContinuousTargets" /* CommandResult.NonContinuousTargets */;
                }
                return this.checkValidations(cmd, (cmd) => this.getTablesOverlappingZones(cmd.sheetId, zones).length
                    ? "TableOverlap" /* CommandResult.TableOverlap */
                    : "Success" /* CommandResult.Success */, (cmd) => this.checkTableConfigUpdateIsValid(cmd.config));
            case "UPDATE_TABLE":
                const updatedTable = this.getCoreTableMatchingTopLeft(cmd.sheetId, cmd.zone);
                if (!updatedTable) {
                    return "TableNotFound" /* CommandResult.TableNotFound */;
                }
                return this.checkValidations(cmd, this.checkUpdatedTableZoneIsValid, (cmd) => this.checkTableConfigUpdateIsValid(cmd.config));
            case "ADD_MERGE":
                for (const table of this.getCoreTables(cmd.sheetId)) {
                    const tableZone = table.range.zone;
                    for (const merge of cmd.target) {
                        if (overlap(tableZone, merge)) {
                            return "MergeInTable" /* CommandResult.MergeInTable */;
                        }
                    }
                }
                break;
        }
        return "Success" /* CommandResult.Success */;
    }
    handle(cmd) {
        switch (cmd.type) {
            case "CREATE_SHEET":
                this.history.update("tables", cmd.sheetId, {});
                break;
            case "DELETE_SHEET": {
                const tables = { ...this.tables };
                delete tables[cmd.sheetId];
                this.history.update("tables", tables);
                break;
            }
            case "DUPLICATE_SHEET": {
                const newTables = {};
                for (const table of this.getCoreTables(cmd.sheetId)) {
                    newTables[table.id] =
                        table.type === "dynamic"
                            ? this.copyDynamicTableForSheet(cmd.sheetIdTo, table)
                            : this.copyStaticTableForSheet(cmd.sheetIdTo, table);
                }
                this.history.update("tables", cmd.sheetIdTo, newTables);
                break;
            }
            case "CREATE_TABLE": {
                const ranges = cmd.ranges.map((rangeData) => this.getters.getRangeFromRangeData(rangeData));
                const union = this.getters.getRangesUnion(ranges);
                const mergesInTarget = this.getters.getMergesInZone(cmd.sheetId, union.zone);
                if (mergesInTarget.length) {
                    this.dispatch("REMOVE_MERGE", { sheetId: cmd.sheetId, target: mergesInTarget });
                }
                const id = this.consumeNextId();
                const config = cmd.config || DEFAULT_TABLE_CONFIG;
                const newTable = cmd.tableType === "dynamic"
                    ? this.createDynamicTable(id, union, config)
                    : this.createStaticTable(id, cmd.tableType, union, config);
                this.history.update("tables", cmd.sheetId, newTable.id, newTable);
                break;
            }
            case "REMOVE_TABLE": {
                const tables = {};
                for (const table of this.getCoreTables(cmd.sheetId)) {
                    if (cmd.target.every((zone) => !intersection(table.range.zone, zone))) {
                        tables[table.id] = table;
                    }
                }
                this.history.update("tables", cmd.sheetId, tables);
                break;
            }
            case "UPDATE_TABLE": {
                this.updateTable(cmd);
                break;
            }
            case "UPDATE_CELL": {
                const sheetId = cmd.sheetId;
                for (const table of this.getCoreTables(sheetId)) {
                    if (table.type === "dynamic") {
                        continue;
                    }
                    const direction = this.canUpdateCellCmdExtendTable(cmd, table);
                    if (direction === "down") {
                        this.extendTableDown(sheetId, table);
                    }
                    else if (direction === "right") {
                        this.extendTableRight(sheetId, table);
                    }
                }
                break;
            }
            case "DELETE_CONTENT": {
                const tables = { ...this.tables[cmd.sheetId] };
                for (const tableId in tables) {
                    const table = tables[tableId];
                    if (table && cmd.target.some((zone) => isZoneInside(table.range.zone, zone))) {
                        this.dispatch("REMOVE_TABLE", { sheetId: cmd.sheetId, target: [table.range.zone] });
                    }
                }
                break;
            }
        }
    }
    getCoreTables(sheetId) {
        return this.tables[sheetId] ? Object.values(this.tables[sheetId]).filter(isDefined) : [];
    }
    getCoreTable({ sheetId, col, row }) {
        return this.getCoreTables(sheetId).find((table) => isInside(col, row, table.range.zone));
    }
    getTablesOverlappingZones(sheetId, zones) {
        return this.getCoreTables(sheetId).filter((table) => zones.some((zone) => overlap(table.range.zone, zone)));
    }
    /** Extend a table down one row */
    extendTableDown(sheetId, table) {
        const newRange = this.getters.extendRange(table.range, "ROW", 1);
        this.history.update("tables", sheetId, table.id, this.updateStaticTable(table, newRange));
    }
    /** Extend a table right one col */
    extendTableRight(sheetId, table) {
        const newRange = this.getters.extendRange(table.range, "COL", 1);
        this.history.update("tables", sheetId, table.id, this.updateStaticTable(table, newRange));
    }
    /**
     * Check if an UpdateCell command should cause the given table to be extended by one row or col.
     *
     * The table should be extended if all of these conditions are true:
     * 1) The updated cell is right below/right of the table
     * 2) The command adds a content to the cell
     * 3) No cell right below/right next to the table had any content before the command
     * 4) Extending the table down/right would not overlap with another table
     * 5) Extending the table down/right would not overlap with a merge
     *
     */
    canUpdateCellCmdExtendTable({ content: newCellContent, sheetId, col, row }, table) {
        if (!newCellContent) {
            return "none";
        }
        const zone = table.range.zone;
        let direction = "none";
        if (zone.bottom + 1 === row && col >= zone.left && col <= zone.right) {
            direction = "down";
        }
        else if (zone.right + 1 === col && row >= zone.top && row <= zone.bottom) {
            direction = "right";
        }
        if (direction === "none") {
            return "none";
        }
        const zoneToCheckIfEmpty = direction === "down"
            ? { ...zone, bottom: zone.bottom + 1, top: zone.bottom + 1 }
            : { ...zone, right: zone.right + 1, left: zone.right + 1 };
        for (let row = zoneToCheckIfEmpty.top; row <= zoneToCheckIfEmpty.bottom; row++) {
            for (let col = zoneToCheckIfEmpty.left; col <= zoneToCheckIfEmpty.right; col++) {
                const cellPosition = { sheetId, col, row };
                // Since this plugin is loaded before CellPlugin, the getters still give us the old cell content
                const cellContent = this.getters.getCell(cellPosition)?.content;
                if (cellContent ||
                    this.getters.isInMerge(cellPosition) ||
                    this.getTablesOverlappingZones(sheetId, [positionToZone(cellPosition)]).length) {
                    return "none";
                }
            }
        }
        return direction;
    }
    getCoreTableMatchingTopLeft(sheetId, zone) {
        for (const table of this.getCoreTables(sheetId)) {
            const tableZone = table.range.zone;
            if (tableZone.left === zone.left && tableZone.top === zone.top) {
                return table;
            }
        }
        return undefined;
    }
    checkUpdatedTableZoneIsValid(cmd) {
        if (!cmd.newTableRange) {
            return "Success" /* CommandResult.Success */;
        }
        const newTableZone = this.getters.getRangeFromRangeData(cmd.newTableRange).zone;
        const zoneIsInSheet = this.getters.checkZonesExistInSheet(cmd.sheetId, [newTableZone]);
        if (zoneIsInSheet !== "Success" /* CommandResult.Success */) {
            return zoneIsInSheet;
        }
        const updatedTable = this.getCoreTableMatchingTopLeft(cmd.sheetId, cmd.zone);
        if (!updatedTable) {
            return "TableNotFound" /* CommandResult.TableNotFound */;
        }
        const overlappingTables = this.getTablesOverlappingZones(cmd.sheetId, [newTableZone]).filter((table) => table.id !== updatedTable.id);
        return overlappingTables.length ? "TableOverlap" /* CommandResult.TableOverlap */ : "Success" /* CommandResult.Success */;
    }
    checkTableConfigUpdateIsValid(config) {
        if (!config) {
            return "Success" /* CommandResult.Success */;
        }
        if (config.numberOfHeaders !== undefined && config.numberOfHeaders < 0) {
            return "InvalidTableConfig" /* CommandResult.InvalidTableConfig */;
        }
        if (config.hasFilters && config.numberOfHeaders === 0) {
            return "InvalidTableConfig" /* CommandResult.InvalidTableConfig */;
        }
        return "Success" /* CommandResult.Success */;
    }
    createStaticTable(id, type, tableRange, config, filters) {
        const zone = tableRange.zone;
        if (!filters) {
            filters = [];
            for (const i of range(zone.left, zone.right + 1)) {
                const filterZone = { ...zone, left: i, right: i };
                const uid = this.consumeNextId();
                filters.push(this.createFilterFromZone(uid, tableRange.sheetId, filterZone, config));
            }
        }
        return {
            id,
            range: tableRange,
            filters,
            config,
            type,
        };
    }
    createDynamicTable(id, tableRange, config) {
        const zone = zoneToTopLeft(tableRange.zone);
        return {
            id,
            range: this.getters.getRangeFromZone(tableRange.sheetId, zone),
            config,
            type: "dynamic",
        };
    }
    updateTable(cmd) {
        const table = this.getCoreTableMatchingTopLeft(cmd.sheetId, cmd.zone);
        if (!table) {
            return;
        }
        const newTableRange = cmd.newTableRange
            ? this.getters.getRangeFromRangeData(cmd.newTableRange)
            : undefined;
        if (newTableRange) {
            const mergesInTarget = this.getters.getMergesInZone(cmd.sheetId, newTableRange.zone);
            this.dispatch("REMOVE_MERGE", { sheetId: cmd.sheetId, target: mergesInTarget });
        }
        const range = newTableRange || table.range;
        const newConfig = this.updateTableConfig(cmd.config, table.config);
        const newTableType = cmd.tableType ?? table.type;
        if ((newTableType === "dynamic" && table.type !== "dynamic") ||
            (newTableType !== "dynamic" && table.type === "dynamic")) {
            const newTable = newTableType === "dynamic"
                ? this.createDynamicTable(table.id, range, newConfig)
                : this.createStaticTable(table.id, newTableType, range, newConfig);
            this.history.update("tables", cmd.sheetId, table.id, newTable);
        }
        else {
            const updatedTable = table.type === "dynamic"
                ? this.updateDynamicTable(table, range, newConfig)
                : this.updateStaticTable(table, range, newConfig, newTableType);
            this.history.update("tables", cmd.sheetId, table.id, updatedTable);
        }
    }
    updateStaticTable(table, newRange, configUpdate, newTableType = table.type) {
        if (newTableType === "dynamic") {
            throw new Error("Cannot use updateStaticTable to update a dynamic table");
        }
        const tableRange = newRange ? newRange : table.range;
        const tableZone = tableRange.zone;
        const newConfig = this.updateTableConfig(configUpdate, table.config);
        const config = newConfig ? newConfig : table.config;
        const filters = [];
        if (newRange || (newConfig && "numberOfHeaders" in newConfig)) {
            for (const i of range(tableZone.left, tableZone.right + 1)) {
                const oldFilter = tableZone.top === table.range.zone.top
                    ? table.filters.find((f) => f.col === i)
                    : undefined;
                const filterZone = { ...tableZone, left: i, right: i };
                const filterId = oldFilter?.id || this.consumeNextId();
                filters.push(this.createFilterFromZone(filterId, tableRange.sheetId, filterZone, config));
            }
        }
        return {
            ...table,
            range: tableRange,
            config,
            filters: filters.length ? filters : table.filters,
            type: newTableType,
        };
    }
    updateDynamicTable(table, newRange, newConfig) {
        const range = newRange
            ? this.getters.getRangeFromZone(newRange.sheetId, zoneToTopLeft(newRange.zone))
            : table.range;
        const config = newConfig ? newConfig : table.config;
        return { ...table, range, config };
    }
    /**
     * Update the old config of a table with the new partial config from an UpdateTable command.
     *
     * Make sure the new config make sense (e.g. if the table has no header, it should not have
     * filters and number of headers should be 0)
     */
    updateTableConfig(update, oldConfig) {
        if (!update) {
            return oldConfig;
        }
        const saneConfig = { ...oldConfig, ...update };
        if (update.numberOfHeaders === 0) {
            saneConfig.hasFilters = false;
        }
        else if (update.hasFilters === true) {
            saneConfig.numberOfHeaders ||= 1;
        }
        return saneConfig;
    }
    createFilterFromZone(id, sheetId, zone, config) {
        const range = this.getters.getRangeFromZone(sheetId, zone);
        return createFilter(id, range, config, this.getters.getRangeFromZone);
    }
    copyStaticTableForSheet(sheetId, table) {
        const newRange = this.getters.getRangeFromZone(sheetId, table.range.zone);
        const newFilters = table.filters.map((filter) => {
            const newFilterRange = this.getters.getRangeFromZone(sheetId, filter.rangeWithHeaders.zone);
            return createFilter(filter.id, newFilterRange, table.config, this.getters.getRangeFromZone);
        });
        return {
            id: table.id,
            range: newRange,
            filters: newFilters,
            config: deepCopy(table.config),
            type: table.type,
        };
    }
    copyDynamicTableForSheet(sheetId, table) {
        const newRange = this.getters.getRangeFromZone(sheetId, table.range.zone);
        return {
            id: table.id,
            range: newRange,
            config: deepCopy(table.config),
            type: "dynamic",
        };
    }
    applyRangeChangeOnTable(sheetId, table, applyChange) {
        const tableRangeChange = applyChange(table.range);
        let newTableRange;
        switch (tableRangeChange.changeType) {
            case "REMOVE":
                this.history.update("tables", sheetId, table.id, undefined);
                return;
            case "NONE":
                return;
            default:
                newTableRange = tableRangeChange.range;
        }
        if (table.type === "dynamic") {
            const newTable = this.updateDynamicTable(table, newTableRange);
            this.history.update("tables", sheetId, table.id, newTable);
            return;
        }
        const filters = [];
        for (const filter of table.filters) {
            const filterRangeChange = applyChange(filter.rangeWithHeaders);
            switch (filterRangeChange.changeType) {
                case "REMOVE":
                    continue;
                case "NONE":
                    filters.push(filter);
                    break;
                default:
                    const newFilterRange = filterRangeChange.range;
                    const newFilter = createFilter(filter.id, newFilterRange, table.config, this.getters.getRangeFromZone);
                    filters.push(newFilter);
            }
        }
        const tableZone = newTableRange.zone;
        if (filters.length < zoneToDimension(tableZone).numberOfCols) {
            for (let col = tableZone.left; col <= tableZone.right; col++) {
                if (!filters.find((filter) => filter.col === col)) {
                    const uid = this.consumeNextId();
                    const filterZone = { ...tableZone, left: col, right: col };
                    filters.push(this.createFilterFromZone(uid, sheetId, filterZone, table.config));
                }
            }
            filters.sort((f1, f2) => f1.col - f2.col);
        }
        const newTable = this.createStaticTable(table.id, table.type, newTableRange, table.config, filters);
        this.history.update("tables", sheetId, table.id, newTable);
    }
    consumeNextId() {
        const id = `${this.nextTableId}`;
        this.history.update("nextTableId", this.nextTableId + 1);
        return id;
    }
    // ---------------------------------------------------------------------------
    // Import/Export
    // ---------------------------------------------------------------------------
    import(data) {
        for (const sheet of data.sheets) {
            for (const tableData of sheet.tables || []) {
                const uuid = this.consumeNextId();
                const tableConfig = tableData.config || DEFAULT_TABLE_CONFIG;
                const range = this.getters.getRangeFromSheetXC(sheet.id, tableData.range);
                const tableType = tableData.type || "static";
                const table = tableType === "dynamic"
                    ? this.createDynamicTable(uuid, range, tableConfig)
                    : this.createStaticTable(uuid, tableType, range, tableConfig);
                this.history.update("tables", sheet.id, table.id, table);
            }
        }
    }
    export(data) {
        for (const sheet of data.sheets) {
            for (const table of this.getCoreTables(sheet.id)) {
                const range = zoneToXc(table.range.zone);
                const tableData = { range, type: table.type };
                if (!deepEquals(table.config, DEFAULT_TABLE_CONFIG)) {
                    tableData.config = table.config;
                }
                sheet.tables.push(tableData);
            }
        }
    }
    exportForExcel(data) {
        for (const sheet of data.sheets) {
            for (const table of this.getCoreTables(sheet.id)) {
                const range = zoneToXc(table.range.zone);
                sheet.tables.push({ range, filters: [], config: table.config });
            }
        }
    }
}

class CarouselPlugin extends CorePlugin {
    static getters = ["getCarousel", "doesCarouselExist"];
    carousels = {};
    allowDispatch(cmd) {
        switch (cmd.type) {
            case "CREATE_CAROUSEL": {
                if (this.getters.getFigure(cmd.sheetId, cmd.figureId)) {
                    return "DuplicatedFigureId" /* CommandResult.DuplicatedFigureId */;
                }
                return "Success" /* CommandResult.Success */;
            }
            case "UPDATE_CAROUSEL": {
                if (!this.carousels[cmd.sheetId]?.[cmd.figureId]) {
                    return "InvalidFigureId" /* CommandResult.InvalidFigureId */;
                }
                return "Success" /* CommandResult.Success */;
            }
        }
        return "Success" /* CommandResult.Success */;
    }
    handle(cmd) {
        switch (cmd.type) {
            case "CREATE_CAROUSEL":
                if (!this.getters.getFigure(cmd.sheetId, cmd.figureId)) {
                    this.dispatch("CREATE_FIGURE", { ...cmd, tag: "carousel" });
                }
                this.history.update("carousels", cmd.sheetId, cmd.figureId, cmd.definition);
                break;
            case "UPDATE_CAROUSEL":
                this.removeDeletedCharts(cmd, this.getters.getCarousel(cmd.figureId).items);
                this.history.update("carousels", cmd.sheetId, cmd.figureId, cmd.definition);
                break;
            case "DUPLICATE_SHEET": {
                const sheetFiguresFrom = this.getters.getFigures(cmd.sheetId);
                for (const fig of sheetFiguresFrom) {
                    if (fig.tag === "carousel") {
                        const figureIdBase = fig.id.split(FIGURE_ID_SPLITTER).pop();
                        const duplicatedFigureId = `${cmd.sheetIdTo}${FIGURE_ID_SPLITTER}${figureIdBase}`;
                        const carousel = this.getCarousel(fig.id);
                        if (carousel) {
                            const size = { width: fig.width, height: fig.height };
                            this.dispatch("CREATE_CAROUSEL", {
                                sheetId: cmd.sheetIdTo,
                                figureId: duplicatedFigureId,
                                offset: fig.offset,
                                col: fig.col,
                                row: fig.row,
                                size,
                                definition: {
                                    items: carousel.items.map((item) => {
                                        if (item.type === "carouselDataView") {
                                            return { ...item };
                                        }
                                        const chartIdBase = item.chartId.split(FIGURE_ID_SPLITTER).pop();
                                        const newChartId = `${cmd.sheetIdTo}${FIGURE_ID_SPLITTER}${chartIdBase}`;
                                        return { ...item, chartId: newChartId };
                                    }),
                                },
                            });
                        }
                    }
                }
                break;
            }
            case "DELETE_FIGURE":
                this.history.update("carousels", cmd.sheetId, cmd.figureId, undefined);
                break;
            case "DELETE_SHEET":
                this.history.update("carousels", cmd.sheetId, undefined);
                break;
        }
    }
    doesCarouselExist(figureId) {
        for (const sheetId in this.carousels) {
            if (this.carousels[sheetId]?.[figureId]) {
                return true;
            }
        }
        return false;
    }
    getCarousel(figureId) {
        for (const sheetId in this.carousels) {
            if (this.carousels[sheetId]?.[figureId]) {
                return this.carousels[sheetId][figureId];
            }
        }
        throw new Error(`There is no carousel with the given figureId: ${figureId}`);
    }
    removeDeletedCharts(cmd, oldItems) {
        const newChartIds = new Set(cmd.definition.items.filter((item) => item.type === "chart").map((item) => item.chartId));
        for (const item of oldItems) {
            if (item.type === "chart" && !newChartIds.has(item.chartId)) {
                this.dispatch("DELETE_CHART", { chartId: item.chartId, sheetId: cmd.sheetId });
            }
        }
    }
    import(data) {
        for (const sheet of data.sheets) {
            const carousels = (sheet.figures || []).filter((figure) => figure.tag === "carousel");
            for (const carousel of carousels) {
                this.history.update("carousels", sheet.id, carousel.id, {
                    items: carousel.data.items,
                    title: carousel.data.title,
                });
            }
        }
    }
    export(data) {
        for (const sheet of data.sheets) {
            const carousels = sheet.figures.filter((figure) => figure.tag === "carousel");
            for (const carousel of carousels) {
                if (this.carousels[sheet.id]?.[carousel.id]) {
                    carousel.data = { ...carousel.data, ...this.carousels[sheet.id]?.[carousel.id] };
                }
            }
        }
    }
}

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

class PivotCorePlugin extends CorePlugin {
    static getters = [
        "getPivotCoreDefinition",
        "getPivotDisplayName",
        "getPivotId",
        "getPivotFormulaId",
        "getPivotIds",
        "getMeasureCompiledFormula",
        "getPivotName",
        "isExistingPivot",
        "getMeasureFullDependencies",
    ];
    nextFormulaId = 1;
    pivots = {};
    formulaIds = {};
    compiledMeasureFormulas = {};
    allowDispatch(cmd) {
        switch (cmd.type) {
            case "ADD_PIVOT": {
                return this.checkValidations(cmd.pivot, this.checkDuplicatedMeasureIds, this.checkSortedColumnInMeasures, this.checkCustomFieldsAreValid);
            }
            case "UPDATE_PIVOT": {
                if (!(cmd.pivotId in this.pivots)) {
                    return "PivotIdNotFound" /* CommandResult.PivotIdNotFound */;
                }
                if (deepEquals(cmd.pivot, this.pivots[cmd.pivotId]?.definition)) {
                    return "NoChanges" /* CommandResult.NoChanges */;
                }
                if (cmd.pivot.name === "") {
                    return "EmptyName" /* CommandResult.EmptyName */;
                }
                return this.checkValidations(cmd.pivot, this.checkDuplicatedMeasureIds, this.checkSortedColumnInMeasures, this.checkCustomFieldsAreValid);
            }
            case "RENAME_PIVOT":
                if (!(cmd.pivotId in this.pivots)) {
                    return "PivotIdNotFound" /* CommandResult.PivotIdNotFound */;
                }
                if (cmd.name === "") {
                    return "EmptyName" /* CommandResult.EmptyName */;
                }
                break;
            case "REMOVE_PIVOT":
            case "DUPLICATE_PIVOT":
            case "INSERT_PIVOT": {
                if (!(cmd.pivotId in this.pivots)) {
                    return "PivotIdNotFound" /* CommandResult.PivotIdNotFound */;
                }
                break;
            }
            case "DUPLICATE_PIVOT":
                if (!(cmd.pivotId in this.pivots)) {
                    return "PivotIdNotFound" /* CommandResult.PivotIdNotFound */;
                }
        }
        return "Success" /* CommandResult.Success */;
    }
    handle(cmd) {
        switch (cmd.type) {
            case "ADD_PIVOT": {
                const { pivotId, pivot } = cmd;
                this.addPivot(pivotId, pivot);
                break;
            }
            case "INSERT_PIVOT": {
                const { sheetId, col, row, pivotId, table } = cmd;
                const position = { sheetId, col, row };
                const { cols, rows, measures, fieldsType } = table;
                const spTable = new SpreadsheetPivotTable(cols, rows, measures, fieldsType || {});
                const formulaId = this.getPivotFormulaId(pivotId);
                this.insertPivot(position, formulaId, spTable);
                break;
            }
            case "RENAME_PIVOT": {
                this.history.update("pivots", cmd.pivotId, "definition", "name", cmd.name);
                break;
            }
            case "REMOVE_PIVOT": {
                const pivots = { ...this.pivots };
                delete pivots[cmd.pivotId];
                const formulaId = this.getPivotFormulaId(cmd.pivotId);
                this.history.update("formulaIds", formulaId, undefined);
                this.history.update("pivots", pivots);
                break;
            }
            case "DUPLICATE_PIVOT": {
                const { pivotId, newPivotId } = cmd;
                const pivot = deepCopy(this.getPivotCore(pivotId).definition);
                pivot.name = cmd.duplicatedPivotName ?? pivot.name + " (copy)";
                this.addPivot(newPivotId, pivot);
                break;
            }
            case "UPDATE_PIVOT": {
                this.history.update("pivots", cmd.pivotId, "definition", deepCopy(cmd.pivot));
                this.compileCalculatedMeasures(cmd.pivotId, cmd.pivot.measures);
                break;
            }
        }
    }
    adaptRanges({ applyChange, adaptFormulaString }) {
        for (const pivotId in this.pivots) {
            const definition = deepCopy(this.pivots[pivotId]?.definition);
            if (!definition) {
                continue;
            }
            const newDefinition = pivotRegistry
                .get(definition.type)
                ?.adaptRanges?.(this.getters, definition, applyChange);
            if (newDefinition && !deepEquals(definition, newDefinition)) {
                this.history.update("pivots", pivotId, "definition", newDefinition);
            }
        }
        for (const pivotId in this.compiledMeasureFormulas) {
            for (const measureId in this.compiledMeasureFormulas[pivotId]) {
                const measure = this.pivots[pivotId]?.definition.measures.find((m) => m.id === measureId);
                if (!measure || !measure.computedBy) {
                    continue;
                }
                const sheetId = measure.computedBy.sheetId;
                const { formula: compiledFormula, dependencies: indirectDependencies } = this.compiledMeasureFormulas[pivotId][measureId];
                // adapt direct dependencies
                this.history.update("compiledMeasureFormulas", pivotId, measureId, "formula", "dependencies", compiledFormula.dependencies.map((range) => applyChange(range).range));
                // adapt all dependencies (including indirect)
                this.history.update("compiledMeasureFormulas", pivotId, measure.id, "dependencies", indirectDependencies.map((range) => applyChange(range).range));
                const oldFormulaString = measure.computedBy.formula;
                const newFormulaString = adaptFormulaString(sheetId, oldFormulaString);
                if (newFormulaString !== oldFormulaString) {
                    this.replaceMeasureFormula(pivotId, measure, newFormulaString);
                }
            }
        }
    }
    // -------------------------------------------------------------------------
    // Getters
    // -------------------------------------------------------------------------
    getPivotDisplayName(pivotId) {
        const formulaId = this.getPivotFormulaId(pivotId);
        return `(#${formulaId}) ${this.getPivotName(pivotId)}`;
    }
    getPivotName(pivotId) {
        return this.getPivotCore(pivotId).definition.name;
    }
    /**
     * Returns the pivot core definition of the pivot with the given id.
     * Be careful, this is the core definition, this should be used only in a
     * context where the pivot is not loaded yet.
     */
    getPivotCoreDefinition(pivotId) {
        return this.getPivotCore(pivotId).definition;
    }
    /**
     * Get the pivot ID (UID) from the formula ID (the one used in the formula)
     */
    getPivotId(formulaId) {
        return this.formulaIds[formulaId];
    }
    getPivotFormulaId(pivotId) {
        return this.getPivotCore(pivotId).formulaId;
    }
    getPivotIds() {
        return Object.keys(this.pivots);
    }
    isExistingPivot(pivotId) {
        return pivotId in this.pivots;
    }
    getMeasureCompiledFormula(pivotId, measure) {
        if (!measure.computedBy) {
            throw new Error(`Measure ${measure.fieldName} is not computed by formula`);
        }
        return this.compiledMeasureFormulas[pivotId][measure.id].formula;
    }
    getMeasureFullDependencies(pivotId, measure) {
        if (!measure.computedBy) {
            throw new Error(`Measure ${measure.fieldName} is not computed by formula`);
        }
        return this.compiledMeasureFormulas[pivotId][measure.id].dependencies;
    }
    // -------------------------------------------------------------------------
    // Private
    // -------------------------------------------------------------------------
    addPivot(pivotId, pivot, formulaId = this.nextFormulaId.toString()) {
        this.history.update("pivots", pivotId, { definition: deepCopy(pivot), formulaId });
        this.compileCalculatedMeasures(pivotId, pivot.measures);
        this.history.update("formulaIds", formulaId, pivotId);
        this.history.update("nextFormulaId", this.nextFormulaId + 1);
    }
    compileCalculatedMeasures(pivotId, measures) {
        for (const measure of measures) {
            if (measure.computedBy) {
                const compiledFormula = this.compileMeasureFormula(measure.computedBy.sheetId, measure.computedBy.formula);
                this.history.update("compiledMeasureFormulas", pivotId, measure.id, "formula", compiledFormula);
            }
        }
        for (const measure of measures) {
            if (measure.computedBy) {
                const dependencies = this.computeMeasureFullDependencies(pivotId, measure);
                this.history.update("compiledMeasureFormulas", pivotId, measure.id, "dependencies", dependencies);
            }
        }
    }
    computeMeasureFullDependencies(pivotId, measure, exploredMeasures = new Set()) {
        const rangeDependencies = [];
        const definition = this.getPivotCoreDefinition(pivotId);
        const formula = this.getMeasureCompiledFormula(pivotId, measure);
        exploredMeasures.add(measure.id);
        for (const token of formula.tokens) {
            if (token.type !== "SYMBOL") {
                continue;
            }
            const otherMeasure = definition.measures.find((measureCandidate) => getCanonicalSymbolName(measureCandidate.id) === token.value &&
                measure.id !== measureCandidate.id);
            if (!otherMeasure || exploredMeasures.has(otherMeasure.id) || !otherMeasure.computedBy) {
                continue;
            }
            rangeDependencies.push(...this.computeMeasureFullDependencies(pivotId, otherMeasure, exploredMeasures));
        }
        rangeDependencies.push(...formula.dependencies.filter((range) => !range.invalidXc));
        return rangeDependencies;
    }
    insertPivot(position, formulaId, table) {
        this.resizeSheet(position.sheetId, position, table);
        const pivotCells = table.getPivotCells();
        for (let col = 0; col < pivotCells.length; col++) {
            for (let row = 0; row < pivotCells[col].length; row++) {
                const pivotCell = pivotCells[col][row];
                this.dispatch("UPDATE_CELL", {
                    sheetId: position.sheetId,
                    col: position.col + col,
                    row: position.row + row,
                    content: createPivotFormula(formulaId, pivotCell),
                });
            }
        }
    }
    resizeSheet(sheetId, { col, row }, table) {
        const colLimit = table.getNumberOfDataColumns() + 1; // +1 for the Top-Left
        const numberCols = this.getters.getNumberCols(sheetId);
        const deltaCol = numberCols - col;
        if (deltaCol < colLimit) {
            this.dispatch("ADD_COLUMNS_ROWS", {
                dimension: "COL",
                base: numberCols - 1,
                sheetId: sheetId,
                sheetName: this.getters.getSheetName(sheetId),
                quantity: colLimit - deltaCol,
                position: "after",
            });
        }
        const rowLimit = table.columns.length + table.rows.length;
        const numberRows = this.getters.getNumberRows(sheetId);
        const deltaRow = numberRows - row;
        if (deltaRow < rowLimit) {
            this.dispatch("ADD_COLUMNS_ROWS", {
                dimension: "ROW",
                base: numberRows - 1,
                sheetId: sheetId,
                sheetName: this.getters.getSheetName(sheetId),
                quantity: rowLimit - deltaRow,
                position: "after",
            });
        }
    }
    getPivotCore(pivotId) {
        const pivot = this.pivots[pivotId];
        if (!pivot) {
            throw new Error(`Pivot with id ${pivotId} not found`);
        }
        return pivot;
    }
    compileMeasureFormula(sheetId, formulaString) {
        const compiledFormula = compile(formulaString);
        const rangeDependencies = compiledFormula.dependencies.map((xc) => this.getters.getRangeFromSheetXC(sheetId, xc));
        return {
            ...compiledFormula,
            dependencies: rangeDependencies,
        };
    }
    replaceMeasureFormula(pivotId, measure, newFormulaString) {
        const pivot = this.pivots[pivotId];
        if (!pivot) {
            return;
        }
        const measureIndex = pivot.definition.measures.indexOf(measure);
        this.history.update("pivots", pivotId, "definition", "measures", measureIndex, "computedBy", {
            formula: newFormulaString,
            sheetId: measure.computedBy.sheetId,
        });
    }
    checkSortedColumnInMeasures(definition) {
        const measures = definition.measures.map((measure) => measure.id);
        if (definition.sortedColumn && !measures.includes(definition.sortedColumn.measure)) {
            return "InvalidDefinition" /* CommandResult.InvalidDefinition */;
        }
        return "Success" /* CommandResult.Success */;
    }
    checkDuplicatedMeasureIds(definition) {
        const uniqueIds = new Set(definition.measures.map((m) => m.id));
        if (definition.measures.length !== uniqueIds.size) {
            return "InvalidDefinition" /* CommandResult.InvalidDefinition */;
        }
        return "Success" /* CommandResult.Success */;
    }
    checkCustomFieldsAreValid(definition) {
        for (const customFieldName in definition.customFields) {
            const customField = definition.customFields[customFieldName];
            const groupedValues = new Set();
            const groupNames = new Set();
            let hasOtherGroup = false;
            for (const group of customField.groups) {
                if (!group.name || groupNames.has(group.name)) {
                    return "InvalidPivotCustomField" /* CommandResult.InvalidPivotCustomField */;
                }
                if (group.values.some((value) => groupedValues.has(value))) {
                    return "InvalidPivotCustomField" /* CommandResult.InvalidPivotCustomField */;
                }
                if (group.isOtherGroup && hasOtherGroup) {
                    return "InvalidPivotCustomField" /* CommandResult.InvalidPivotCustomField */;
                }
                group.values.forEach((value) => groupedValues.add(value));
                groupNames.add(group.name);
                hasOtherGroup ||= !!group.isOtherGroup;
            }
        }
        return "Success" /* CommandResult.Success */;
    }
    // ---------------------------------------------------------------------
    // Import/Export
    // ---------------------------------------------------------------------
    /**
     * Import the pivots
     */
    import(data) {
        if (data.pivots) {
            for (const [id, pivot] of Object.entries(data.pivots)) {
                this.addPivot(id, pivot, pivot.formulaId);
            }
        }
        this.history.update("nextFormulaId", data.pivotNextId || getMaxObjectId(this.pivots) + 1);
    }
    /**
     * Export the pivots
     */
    export(data) {
        data.pivots = {};
        for (const pivotId in this.pivots) {
            data.pivots[pivotId] = {
                ...this.getPivotCoreDefinition(pivotId),
                formulaId: this.getPivotFormulaId(pivotId),
            };
        }
        data.pivotNextId = this.nextFormulaId;
    }
}

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

class SpreadsheetPivotCorePlugin extends CorePlugin {
    allowDispatch(cmd) {
        switch (cmd.type) {
            case "ADD_PIVOT":
            case "UPDATE_PIVOT":
                const definition = cmd.pivot;
                return this.checkDataSetValidity(definition);
        }
        return "Success" /* CommandResult.Success */;
    }
    checkDataSetValidity(definition) {
        if (definition.type === "SPREADSHEET" && definition.dataSet) {
            const { zone, sheetId } = definition.dataSet;
            if (!sheetId || !this.getters.tryGetSheet(sheetId) || !zone || !isZoneValid(zone)) {
                return "InvalidDataSet" /* CommandResult.InvalidDataSet */;
            }
            return this.getters.checkZonesExistInSheet(sheetId, [zone]);
        }
        return "Success" /* CommandResult.Success */;
    }
}

class TableStylePlugin extends CorePlugin {
    static getters = [
        "getNewCustomTableStyleName",
        "getTableStyle",
        "getTableStyles",
        "isTableStyleEditable",
    ];
    styles = {};
    allowDispatch(cmd) {
        switch (cmd.type) {
            case "CREATE_TABLE":
            case "UPDATE_TABLE":
                if (cmd.config?.styleId && !this.styles[cmd.config.styleId]) {
                    return "InvalidTableConfig" /* CommandResult.InvalidTableConfig */;
                }
                break;
            case "CREATE_TABLE_STYLE":
                if (!TABLE_STYLES_TEMPLATES[cmd.templateName]) {
                    return "InvalidTableStyle" /* CommandResult.InvalidTableStyle */;
                }
                try {
                    toHex(cmd.primaryColor);
                }
                catch (e) {
                    return "InvalidTableStyle" /* CommandResult.InvalidTableStyle */;
                }
                break;
        }
        return "Success" /* CommandResult.Success */;
    }
    handle(cmd) {
        switch (cmd.type) {
            case "CREATE_TABLE_STYLE":
                const style = buildTableStyle(cmd.tableStyleName, cmd.templateName, cmd.primaryColor);
                this.history.update("styles", cmd.tableStyleId, style);
                break;
            case "REMOVE_TABLE_STYLE":
                const styles = { ...this.styles };
                delete styles[cmd.tableStyleId];
                this.history.update("styles", styles);
                for (const sheetId of this.getters.getSheetIds()) {
                    for (const table of this.getters.getCoreTables(sheetId)) {
                        if (table.config.styleId === cmd.tableStyleId) {
                            this.dispatch("UPDATE_TABLE", {
                                sheetId,
                                zone: table.range.zone,
                                config: { styleId: DEFAULT_TABLE_CONFIG.styleId },
                            });
                        }
                    }
                }
                break;
        }
    }
    getTableStyle(styleId) {
        if (!this.styles[styleId]) {
            throw new Error(`Table style ${styleId} does not exist`);
        }
        return this.styles[styleId];
    }
    getTableStyles() {
        return this.styles;
    }
    getNewCustomTableStyleName() {
        const name = _t("Custom Table Style");
        const styleNames = Object.values(this.styles).map((style) => style.displayName);
        return getUniqueText(name, styleNames, { compute: (name, i) => `${name} ${i}`, start: 2 });
    }
    isTableStyleEditable(styleId) {
        return !TABLE_PRESETS[styleId];
    }
    import(data) {
        for (const presetStyleId in TABLE_PRESETS) {
            this.styles[presetStyleId] = TABLE_PRESETS[presetStyleId];
        }
        for (const styleId in data.customTableStyles) {
            const styleData = data.customTableStyles[styleId];
            this.styles[styleId] = buildTableStyle(styleData.displayName, styleData.templateName, styleData.primaryColor);
        }
    }
    export(data) {
        const exportedStyles = {};
        for (const styleId in this.styles) {
            if (!TABLE_PRESETS[styleId]) {
                exportedStyles[styleId] = {
                    displayName: this.styles[styleId].displayName,
                    templateName: this.styles[styleId].templateName,
                    primaryColor: this.styles[styleId].primaryColor,
                };
            }
        }
        data.customTableStyles = exportedStyles;
    }
}

/**
 * Core view plugins handle any data derived from core date (i.e. evaluation).
 * They cannot impact the model data (i.e. cannot dispatch commands).
 */
class CoreViewPlugin extends BasePlugin {
    getters;
    constructor({ getters, stateObserver }) {
        super(stateObserver);
        this.getters = getters;
    }
}

/**
 * This registry is used to register functions that should be called after each iteration of the evaluation.
 * This is use currently to mark the each pivot to be re-evaluated. We have to do this after each iteration
 * to avoid to reload the data of the pivot at each function call (PIVOT.VALUE and PIVOT.HEADER). After each
 * evaluation iteration, the pivot has to be re-evaluated during the next iteration.
 */
const onIterationEndEvaluationRegistry = new Registry();
onIterationEndEvaluationRegistry.add("pivots", (getters) => {
    for (const pivotId of getters.getPivotIds()) {
        const pivot = getters.getPivot(pivotId);
        pivot.markAsDirtyForEvaluation?.();
    }
});

const functionMap = functionRegistry.mapping;
/**
 * Return all functions necessary to properly evaluate a formula:
 * - a refFn function to read any reference, cell or range of a normalized formula
 * - a range function to convert any reference to a proper value array
 * - an evaluation context
 */
function buildCompilationParameters(context, getters, computeCell) {
    const builder = new CompilationParametersBuilder(context, getters, computeCell);
    return builder.getParameters();
}
class CompilationParametersBuilder {
    getters;
    computeCell;
    evalContext;
    rangeCache = {};
    constructor(context, getters, computeCell) {
        this.getters = getters;
        this.computeCell = computeCell;
        this.evalContext = Object.assign(Object.create(functionMap), context, {
            getters: this.getters,
            locale: this.getters.getLocale(),
        });
    }
    getParameters() {
        return {
            referenceDenormalizer: this.refFn.bind(this),
            ensureRange: this.range.bind(this),
            evalContext: this.evalContext,
        };
    }
    /**
     * Returns the value of the cell(s) used in reference
     *
     * @param range the references used
     * @param isMeta if a reference is supposed to be used in a `meta` parameter as described in the
     *        function for which this parameter is used, we just return the string of the parameter.
     *        The `compute` of the formula's function must process it completely
     */
    refFn(range, isMeta) {
        const rangeError = this.getRangeError(range);
        if (rangeError) {
            return rangeError;
        }
        // the compiler guarantees only single cell ranges reach this part of the code
        const position = { sheetId: range.sheetId, col: range.zone.left, row: range.zone.top };
        if (isMeta) {
            this.computeCell(position); // ensure the cell is computed if the function using the meta parameter uses the value
            // Use zoneToXc of zone instead of getRangeString to avoid sending unbounded ranges
            const sheetName = this.getters.getSheetName(range.sheetId);
            return { value: getFullReference(sheetName, zoneToXc(range.zone)) };
        }
        return this.computeCell(position);
    }
    /**
     * Return the values of the cell(s) used in reference, but always in the format of a range even
     * if a single cell is referenced. It is a list of col values. This is useful for the formulas that describe parameters as
     * range<number> etc.
     *
     * Note that each col is possibly sparse: it only contain the values of cells
     * that are actually present in the grid.
     */
    range(range, isMeta) {
        const rangeError = this.getRangeError(range);
        if (rangeError) {
            return [[rangeError]];
        }
        const sheetId = range.sheetId;
        const zone = range.zone;
        // Performance issue: Avoid fetching data on positions that are out of the spreadsheet
        // e.g. A1:ZZZ9999 in a sheet with 10 cols and 10 rows should ignore everything past J10 and return a 10x10 array
        const sheetZone = this.getters.getSheetZone(sheetId);
        const _zone = intersection(zone, sheetZone);
        if (!_zone) {
            return [[]];
        }
        const { top, left, bottom, right } = zone;
        const cacheKey = `${sheetId}-${top}-${left}-${bottom}-${right}-${isMeta}`;
        if (cacheKey in this.rangeCache) {
            return this.rangeCache[cacheKey];
        }
        const height = _zone.bottom - _zone.top + 1;
        const width = _zone.right - _zone.left + 1;
        const matrix = new Array(width);
        const sheetName = this.getters.getSheetName(range.sheetId);
        // Performance issue: nested loop is faster than a map here
        for (let col = _zone.left; col <= _zone.right; col++) {
            const colIndex = col - _zone.left;
            matrix[colIndex] = new Array(height);
            for (let row = _zone.top; row <= _zone.bottom; row++) {
                const rowIndex = row - _zone.top;
                const computedCell = this.computeCell({ sheetId, col, row });
                matrix[colIndex][rowIndex] = isMeta
                    ? { value: getFullReference(sheetName, toXC(col, row)) }
                    : computedCell;
            }
        }
        this.rangeCache[cacheKey] = matrix;
        return matrix;
    }
    getRangeError(range) {
        if (!isZoneValid(range.zone)) {
            return new InvalidReferenceError();
        }
        if (range.invalidSheetName) {
            return new InvalidReferenceError(_t("Invalid sheet name: %s", range.invalidSheetName));
        }
        return undefined;
    }
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        return result;
    }

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

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

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

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

        return false;
    }

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

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

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

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

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

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

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

        return this;
    }

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

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

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

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

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

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

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

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

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

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

            } else node = null; // nothing found
        }

        return this;
    }

    toBBox(item) { return item; }

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

    toJSON() { return this.data; }

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

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

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

    _build(items, left, right, height) {

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

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

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

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

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

        // split the items into M mostly square tiles

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

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

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

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

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

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

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

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

        calcBBox(node, this.toBBox);

        return node;
    }

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

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

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

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

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

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

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

        return node;
    }

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

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

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

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

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

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

        this._chooseSplitAxis(node, m, M);

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

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

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

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

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

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

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

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

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

                minArea = area < minArea ? area : minArea;

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

        return index || M - m;
    }

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

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

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

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

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

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

        return margin;
    }

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

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

                } else this.clear();

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

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

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

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

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

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

    return destNode;
}

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

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

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

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

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

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

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

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

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

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

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

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

        if (right - left <= n) continue;

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

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

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

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

class ZoneSet {
    profilesStartingPosition = [0];
    profiles = new Map([[0, []]]);
    constructor(zones = []) {
        for (const zone of zones) {
            this.add(zone);
        }
    }
    isEmpty() {
        return this.profiles.size === 1 && this.profiles.get(0)?.length === 0;
    }
    add(zone) {
        modifyProfiles(this.profilesStartingPosition, this.profiles, [zone]);
    }
    delete(zone) {
        modifyProfiles(this.profilesStartingPosition, this.profiles, [zone], true);
    }
    has(zone) {
        return profilesContainsZone(this.profilesStartingPosition, this.profiles, zone);
    }
    difference(other) {
        const result = this.copy();
        for (const zone of other) {
            result.delete(zone);
        }
        return result;
    }
    copy() {
        const result = new ZoneSet();
        result.profilesStartingPosition = [...this.profilesStartingPosition];
        result.profiles = new Map();
        for (const [key, value] of this.profiles) {
            result.profiles.set(key, [...value]);
        }
        return result;
    }
    /**
     * iterator of all the zones in the ZoneSet
     */
    [Symbol.iterator]() {
        return constructZonesFromProfiles(this.profilesStartingPosition, this.profiles)[Symbol.iterator]();
    }
}

class RangeSet {
    setsBySheetId = {};
    constructor(ranges = []) {
        for (const range of ranges) {
            this.add(range);
        }
    }
    add(range) {
        if (!this.setsBySheetId[range.sheetId]) {
            this.setsBySheetId[range.sheetId] = new ZoneSet();
        }
        this.setsBySheetId[range.sheetId].add(range.zone);
    }
    addMany(ranges) {
        for (const range of ranges) {
            this.add(range);
        }
    }
    addPosition(position) {
        this.add(positionToBoundedRange(position));
    }
    addManyPositions(positions) {
        for (const position of positions) {
            this.addPosition(position);
        }
    }
    has(range) {
        if (!this.setsBySheetId[range.sheetId]) {
            return false;
        }
        return this.setsBySheetId[range.sheetId].has(range.zone);
    }
    hasPosition(position) {
        return this.has(positionToBoundedRange(position));
    }
    delete(range) {
        if (!this.setsBySheetId[range.sheetId]) {
            return;
        }
        this.setsBySheetId[range.sheetId].delete(range.zone);
    }
    deleteMany(ranges) {
        for (const range of ranges) {
            this.delete(range);
        }
    }
    deleteManyPositions(positions) {
        for (const position of positions) {
            this.delete(positionToBoundedRange(position));
        }
    }
    difference(other) {
        const result = new RangeSet();
        for (const sheetId in this.setsBySheetId) {
            result.setsBySheetId[sheetId] = this.setsBySheetId[sheetId];
        }
        for (const sheetId in other.setsBySheetId) {
            if (result.setsBySheetId[sheetId]) {
                result.setsBySheetId[sheetId] = result.setsBySheetId[sheetId].difference(other.setsBySheetId[sheetId]);
            }
        }
        return result;
    }
    copy() {
        const result = new RangeSet();
        for (const sheetId in this.setsBySheetId) {
            result.setsBySheetId[sheetId] = this.setsBySheetId[sheetId].copy();
        }
        return result;
    }
    clear() {
        this.setsBySheetId = {};
    }
    isEmpty() {
        for (const sheetId in this.setsBySheetId) {
            if (!this.setsBySheetId[sheetId].isEmpty()) {
                return false;
            }
        }
        return true;
    }
    /**
     * iterator of all the ranges in the RangeSet
     */
    [Symbol.iterator]() {
        const result = [];
        for (const sheetId in this.setsBySheetId) {
            for (const zone of this.setsBySheetId[sheetId]) {
                result.push({ sheetId: sheetId, zone });
            }
        }
        return result[Symbol.iterator]();
    }
}

/**
 * R-Tree of ranges, mapping zones (r-tree bounding boxes) to ranges (data of the r-tree item).
 * Ranges associated to the exact same bounding box are grouped together
 * to reduce the number of nodes in the R-tree.
 */
class DependenciesRTree {
    rTree;
    constructor(items = []) {
        const compactedBoxes = groupSameBoundingBoxes(items);
        this.rTree = new SpreadsheetRTree(compactedBoxes);
    }
    insert(item) {
        const data = this.rTree.search(item.boundingBox);
        const itemBoundingBox = item.boundingBox;
        const exactBoundingBox = data.find(({ boundingBox }) => boundingBox.sheetId === itemBoundingBox.sheetId &&
            boundingBox.zone.left === itemBoundingBox.zone.left &&
            boundingBox.zone.top === itemBoundingBox.zone.top &&
            boundingBox.zone.right === itemBoundingBox.zone.right &&
            boundingBox.zone.bottom === itemBoundingBox.zone.bottom);
        if (exactBoundingBox) {
            exactBoundingBox.data.add(item.data);
        }
        else {
            this.rTree.insert({ ...item, data: new RangeSet([item.data]) });
        }
    }
    search({ zone, sheetId }) {
        const results = new RangeSet();
        for (const { data } of this.rTree.search({ zone, sheetId })) {
            results.addMany(data);
        }
        return results;
    }
    remove(item) {
        const data = this.rTree.search(item.boundingBox);
        const itemBoundingBox = item.boundingBox;
        const exactBoundingBox = data.find(({ boundingBox }) => boundingBox.sheetId === itemBoundingBox.sheetId &&
            boundingBox.zone.left === itemBoundingBox.zone.left &&
            boundingBox.zone.top === itemBoundingBox.zone.top &&
            boundingBox.zone.right === itemBoundingBox.zone.right &&
            boundingBox.zone.bottom === itemBoundingBox.zone.bottom);
        if (exactBoundingBox) {
            exactBoundingBox.data.delete(item.data);
        }
        else {
            this.rTree.remove({ ...item, data: new RangeSet([item.data]) });
        }
    }
}
/**
 * Group together all formulas pointing to the exact same dependency (bounding box).
 * The goal is to optimize the following case:
 * - if any cell in B1:B1000 changes, C1 must be recomputed
 * - if any cell in B1:B1000 changes, C2 must be recomputed
 * - if any cell in B1:B1000 changes, C3 must be recomputed
 * ...
 * - if any cell in B1:B1000 changes, C1000 must be recomputed
 *
 * Instead of having 1000 entries in the R-tree, we want to have a single entry
 * with B1:B1000 (bounding box) pointing to C1:C1000 (formulas).
 */
function groupSameBoundingBoxes(items) {
    // Important: this function must be as fast as possible. It is on the evaluation hot path.
    let maxCol = 0;
    let maxRow = 0;
    for (let i = 0; i < items.length; i++) {
        const zone = items[i].boundingBox.zone;
        if (zone.right > maxCol) {
            maxCol = zone.right;
        }
        if (zone.bottom > maxRow) {
            maxRow = zone.bottom;
        }
    }
    maxCol += 1;
    maxRow += 1;
    // in most real-world cases, we can use a fast numeric key
    // but if the zones are too far right or bottom, we fallback to a slower string key
    const maxPossibleKey = (((maxRow + 1) * maxCol + 1) * maxRow + 1) * maxCol;
    const useFastKey = maxPossibleKey <= Number.MAX_SAFE_INTEGER;
    if (!useFastKey) {
        console.warn("Max col/row size exceeded, using slow zone key");
    }
    const groupedByBBox = {};
    for (const item of items) {
        const sheetId = item.boundingBox.sheetId;
        if (!groupedByBBox[sheetId]) {
            groupedByBBox[sheetId] = {};
        }
        const bBox = item.boundingBox.zone;
        let bBoxKey = 0;
        if (useFastKey) {
            bBoxKey =
                bBox.left +
                    bBox.top * maxCol +
                    bBox.right * maxCol * maxRow +
                    bBox.bottom * maxCol * maxRow * maxCol;
        }
        else {
            bBoxKey = `${bBox.left},${bBox.top},${bBox.right},${bBox.bottom}`;
        }
        if (groupedByBBox[sheetId][bBoxKey]) {
            const ranges = groupedByBBox[sheetId][bBoxKey].data;
            ranges.add(item.data);
        }
        else {
            groupedByBBox[sheetId][bBoxKey] = {
                boundingBox: item.boundingBox,
                data: new RangeSet([item.data]),
            };
        }
    }
    const result = [];
    for (const sheetId in groupedByBBox) {
        const map = groupedByBBox[sheetId];
        for (const key in map) {
            result.push(map[key]);
        }
    }
    return result;
}

/**
 * Implementation of a dependency Graph.
 * The graph is used to evaluate the cells in the correct
 * order, and should be updated each time a cell's content is modified
 *
 * It uses an R-Tree data structure to efficiently find dependent cells.
 */
class FormulaDependencyGraph {
    dependencies = new PositionMap();
    rTree;
    constructor(data = []) {
        this.rTree = new DependenciesRTree(data);
    }
    removeAllDependencies(formulaPosition) {
        const ranges = this.dependencies.get(formulaPosition);
        if (!ranges) {
            return;
        }
        for (const range of ranges) {
            this.rTree.remove(range);
        }
        this.dependencies.delete(formulaPosition);
    }
    addDependencies(formulaPosition, dependencies) {
        const rTreeItems = dependencies.map(({ sheetId, zone }) => ({
            data: {
                sheetId: formulaPosition.sheetId,
                zone: positionToZone(formulaPosition),
            },
            boundingBox: {
                zone,
                sheetId,
            },
        }));
        for (const item of rTreeItems) {
            this.rTree.insert(item);
        }
        const existingDependencies = this.dependencies.get(formulaPosition);
        if (existingDependencies) {
            existingDependencies.push(...rTreeItems);
        }
        else {
            this.dependencies.set(formulaPosition, rTreeItems);
        }
    }
    /**
     * Return all the cells that depend on the provided ranges.
     */
    getCellsDependingOn(ranges, visited = new RangeSet()) {
        visited = visited.copy();
        const queue = Array.from(ranges).reverse();
        while (queue.length > 0) {
            const range = queue.pop();
            visited.add(range);
            const impactedRanges = this.rTree.search(range);
            queue.push(...impactedRanges.difference(visited));
        }
        // remove initial ranges
        for (const range of ranges) {
            visited.delete(range);
        }
        return visited;
    }
}

/**
 * Implements a fixed-sized grid or 2D matrix of bits.
 * based on https://github.com/zandaqo/structurae
 *
 * The grid is implemented as a 1D array of 32-bit integers, where each bit represents a cell in the grid.
 * It follows row-major order, with each row stored consecutively in 32-bit blocks.
 * Pads the number of columns to the next power of 2 to allow quick lookups with bitwise operations.
 *
 * Key terminology:
 * - bucket: Index of an item in the Uint32Array, a 32-bit integer.
 * - bitPosition: The position of a bit within the bucket 32-bit integer.
 */
class BinaryGrid extends Uint32Array {
    columnOffset = 0;
    cols = 0;
    rows = 0;
    /**
     * Creates a binary grid of specified dimensions.
     */
    static create(rows, columns) {
        const columnOffset = log2Ceil(columns);
        const length = (rows << columnOffset) >> 5;
        const grid = new this(length + 1);
        grid.columnOffset = columnOffset;
        grid.cols = columns;
        grid.rows = rows;
        return grid;
    }
    /**
     * Returns the bit at given coordinates.
     */
    getValue(position) {
        const [bucket, bitPosition] = this.getCoordinates(position);
        return ((this[bucket] >> bitPosition) & 1);
    }
    /**
     * Sets the bit at given coordinates.
     */
    setValue(position, value) {
        const [bucket, bitPosition] = this.getCoordinates(position);
        const currentValue = (this[bucket] >> bitPosition) & 1;
        const hasBeenInserted = currentValue === 0 && value === 1;
        this[bucket] = (this[bucket] & ~(1 << bitPosition)) | (value << bitPosition);
        return hasBeenInserted;
        // Let's breakdown of the above line:
        // with an example with a 4-bit integer (instead of 32-bit).
        //
        // Let's say we want to set the bit at position 2 to 1 and the existing
        // bit sequence this[bucket] is 1001. The final bit sequence should be 1101.
        //
        // First, we clear the bit at position 2 by AND-ing this[bucket] with a
        // mask having all 1s except a 0 at the bit position (~ (1 << bitPosition)).
        // 1 << bitPosition is 0100 (shifting 0001 to the left by 2)
        // Inverting the bits with ~ gives the final mask ~(1 << bitPosition): 1011
        //
        // Then, we shift the value by the bit position (value << bitPosition: 0100)
        // and OR the result with the previous step's result:
        // (1001 & 1011) | 0100 = 1101
    }
    isEmpty() {
        return !this.some((bucket) => bucket !== 0);
    }
    fillAllPositions() {
        const thirtyTwoOnes = -1 >>> 0; // same as 2 ** 32 - 1, a 32-bit number with all bits set to 1
        this.fill(thirtyTwoOnes);
    }
    clear() {
        this.fill(0);
    }
    getCoordinates(position) {
        const { row, col } = position;
        const index = (row << this.columnOffset) + col;
        const bucket = index >> 5;
        return [bucket, index - (bucket << 5)];
    }
}
function log2Ceil(value) {
    // A faster version of Math.ceil(Math.log2(value)).
    if (value === 0) {
        return -Infinity;
    }
    else if (value < 0) {
        return NaN;
    }
    // --value handles the case where value is a power of 2
    return 32 - Math.clz32(--value);
}

class PositionSet {
    sheets = {};
    /**
     * List of positions in the order they were inserted.
     */
    insertions = [];
    maxSize = 0;
    constructor(sheetSizes) {
        for (const sheetId in sheetSizes) {
            const cols = sheetSizes[sheetId].cols;
            const rows = sheetSizes[sheetId].rows;
            this.maxSize += cols * rows;
            this.sheets[sheetId] = BinaryGrid.create(rows, cols);
        }
    }
    add(position) {
        const hasBeenInserted = this.sheets[position.sheetId].setValue(position, 1);
        if (hasBeenInserted) {
            this.insertions.push(position);
        }
    }
    addMany(positions) {
        for (const position of positions) {
            this.add(position);
        }
    }
    delete(position) {
        this.sheets[position.sheetId].setValue(position, 0);
    }
    deleteMany(positions) {
        for (const position of positions) {
            this.delete(position);
        }
    }
    has(position) {
        return this.sheets[position.sheetId].getValue(position) === 1;
    }
    clear() {
        const insertions = [...this];
        this.insertions = [];
        for (const sheetId in this.sheets) {
            this.sheets[sheetId].clear();
        }
        return insertions;
    }
    isEmpty() {
        if (this.insertions.length === 0) {
            return true;
        }
        for (const sheetId in this.sheets) {
            if (!this.sheets[sheetId].isEmpty()) {
                return false;
            }
        }
        return true;
    }
    fillAllPositions() {
        this.insertions = new Array(this.maxSize);
        let index = 0;
        for (const sheetId in this.sheets) {
            const grid = this.sheets[sheetId];
            grid.fillAllPositions();
            for (let i = 0; i < grid.rows; i++) {
                for (let j = 0; j < grid.cols; j++) {
                    this.insertions[index++] = { sheetId, row: i, col: j };
                }
            }
        }
    }
    /**
     * Iterate over the positions in the order of insertion.
     * Note that the same position may be yielded multiple times if the value was added
     * to the set then removed and then added again.
     */
    *[Symbol.iterator]() {
        for (const position of this.insertions) {
            if (this.sheets[position.sheetId].getValue(position) === 1) {
                yield position;
            }
        }
    }
}

/**
 * Contains, for each cell, the array
 * formulas that could potentially spread on it
 * This is essentially a two way mapping between array formulas
 * and their results.
 *
 * As we don't allow two array formulas to spread on the same cell, this structure
 * is used to force the reevaluation of the potential spreaders of a cell when the
 * content of this cell is modified. This structure should be updated each time
 * an array formula is evaluated and try to spread on another cell.
 *
 */
class SpreadingRelation {
    /**
     * Internal structure:
     * For something like
     * - A2:'=SPLIT("KAYAK", "A")'
     * - B1:'=TRANSPOSE(SPLIT("COYOTE", "O"))'
     *
     * Resulting in:
     * ```
     * -----------------
     * |   | A | B | C |
     * |---+---+---+---|
     * | 1 |   | C |   |
     * | 2 | K | Y | K |
     * | 3 |   | T |   |
     * | 4 |   | E |   |
     * -----------------
     * ```
     * We have `resultsToArrayFormulas` is an R-tree looking like:
     * - (A2:C2) --> A2     meaning values in A2:C2 are the result of A2
     * - (B1:B4) --> B1     meaning values in B1:B4 are the result of B1
     *
     * Note that B2 is part of both zones because it can be the result of
     * A2 or B1.
     * Using an R-tree allows for fast insertions while still having
     * a relatively fast lookup.
     *
     * We have `arrayFormulasToResults` looking like:
     * - (A2) --> A2:C2     meaning A2 spreads on the zone A2:C2
     * - (B1) --> B1:B4     meaning B1 spreads on the zone B1:B4
     *
     */
    resultsToArrayFormulas = new SpreadsheetRTree();
    arrayFormulasToResults = new PositionMap();
    searchFormulaPositionsSpreadingOn(sheetId, zone) {
        return (this.resultsToArrayFormulas.search({ sheetId, zone }).map((node) => node.data) || EMPTY_ARRAY);
    }
    getArrayResultZone(formulasPosition) {
        return this.arrayFormulasToResults.get(formulasPosition);
    }
    /**
     * Remove a spreading relation for a given array formula position
     * and its result zone
     */
    removeNode(position) {
        const resultZone = this.arrayFormulasToResults.get(position);
        if (!resultZone) {
            return;
        }
        this.resultsToArrayFormulas.remove({
            boundingBox: { sheetId: position.sheetId, zone: resultZone },
            data: position,
        });
        this.arrayFormulasToResults.delete(position);
    }
    /**
     * Create a spreading relation between two cells
     */
    addRelation({ arrayFormulaPosition, resultZone: resultPosition, }) {
        this.resultsToArrayFormulas.insert({
            boundingBox: { sheetId: arrayFormulaPosition.sheetId, zone: resultPosition },
            data: arrayFormulaPosition,
        });
        this.arrayFormulasToResults.set(arrayFormulaPosition, resultPosition);
    }
    isArrayFormula(position) {
        return this.arrayFormulasToResults.has(position);
    }
}
const EMPTY_ARRAY = [];

const MAX_ITERATION = 30;
const ERROR_CYCLE_CELL = Object.freeze(createEvaluatedCell({ ...new CircularDependencyError(), origin: undefined }));
const EMPTY_CELL = Object.freeze(createEvaluatedCell({ value: null }));
class Evaluator {
    context;
    getters;
    compilationParams;
    evaluatedCells = new PositionMap();
    formulaDependencies = lazy(new FormulaDependencyGraph());
    blockedArrayFormulas = new PositionSet({});
    spreadingRelations = new SpreadingRelation();
    constructor(context, getters) {
        this.context = context;
        this.getters = getters;
        this.compilationParams = buildCompilationParameters(this.context, this.getters, this.computeAndSave.bind(this));
    }
    getEvaluatedCell(position) {
        return this.evaluatedCells.get(position) || EMPTY_CELL;
    }
    getSpreadZone(position, options = { ignoreSpillError: false }) {
        const spreadZone = this.spreadingRelations.getArrayResultZone(position);
        if (!spreadZone) {
            return undefined;
        }
        const evaluatedCell = this.evaluatedCells.get(position);
        if (evaluatedCell?.type === CellValueType.error &&
            !(options.ignoreSpillError && evaluatedCell?.value === CellErrorType.SpilledBlocked)) {
            return positionToZone(position);
        }
        return union(positionToZone(position), spreadZone);
    }
    getEvaluatedPositions() {
        return this.evaluatedCells.keys();
    }
    getEvaluatedPositionsInSheet(sheetId) {
        return this.evaluatedCells.keysForSheet(sheetId);
    }
    getArrayFormulaSpreadingOn(position) {
        const isEmpty = this.getEvaluatedCell(position).type === CellValueType.empty;
        if (isEmpty) {
            return undefined;
        }
        const arrayFormulas = this.spreadingRelations.searchFormulaPositionsSpreadingOn(position.sheetId, positionToZone(position));
        return arrayFormulas.find((position) => !this.blockedArrayFormulas.has(position));
    }
    isArrayFormulaSpillBlocked(position) {
        return this.blockedArrayFormulas.has(position);
    }
    updateDependencies(position) {
        // removing dependencies is slow because it requires
        // to traverse the entire r-tree.
        // The data structure is optimized for searches the other way around
        this.formulaDependencies().removeAllDependencies(position);
        const dependencies = this.getDirectDependencies(position);
        this.formulaDependencies().addDependencies(position, dependencies);
    }
    addDependencies(position, dependencies) {
        this.formulaDependencies().addDependencies(position, dependencies);
        for (const range of dependencies) {
            // ensure that all ranges are computed
            this.compilationParams.ensureRange(range, false);
        }
    }
    updateCompilationParametersForIsolatedFormula(originCellPosition) {
        this.compilationParams = buildCompilationParameters(this.context, this.getters, this.computeAndSave.bind(this));
        this.compilationParams.evalContext.__originCellPosition = originCellPosition;
        this.compilationParams.evalContext.updateDependencies = undefined;
        this.compilationParams.evalContext.addDependencies = undefined;
        this.compilationParams.evalContext.lookupCaches = this.compilationParams.evalContext
            .lookupCaches || {
            forwardSearch: new Map(),
            reverseSearch: new Map(),
        };
    }
    updateCompilationParameters() {
        // rebuild the compilation parameters (with a clean cache)
        this.compilationParams = buildCompilationParameters(this.context, this.getters, this.computeAndSave.bind(this));
        this.compilationParams.evalContext.updateDependencies = this.updateDependencies.bind(this);
        this.compilationParams.evalContext.addDependencies = this.addDependencies.bind(this);
        this.compilationParams.evalContext.lookupCaches = {
            forwardSearch: new Map(),
            reverseSearch: new Map(),
        };
    }
    createEmptyPositionSet() {
        const sheetSizes = {};
        for (const sheetId of this.getters.getSheetIds()) {
            sheetSizes[sheetId] = {
                rows: this.getters.getNumberRows(sheetId),
                cols: this.getters.getNumberCols(sheetId),
            };
        }
        return new PositionSet(sheetSizes);
    }
    evaluateCells(positions) {
        const start = performance.now();
        const rangesToCompute = new RangeSet();
        rangesToCompute.addManyPositions(positions);
        const arrayFormulasPositions = this.getArrayFormulasImpactedByChangesOf(positions);
        rangesToCompute.addMany(this.getCellsDependingOn(rangesToCompute));
        rangesToCompute.addMany(arrayFormulasPositions);
        rangesToCompute.addMany(this.getCellsDependingOn(arrayFormulasPositions));
        this.evaluate(rangesToCompute);
        console.debug("evaluate Cells", performance.now() - start, "ms");
    }
    getArrayFormulasImpactedByChangesOf(positions) {
        const impactedRanges = new RangeSet();
        for (const position of positions) {
            const content = this.getters.getCell(position)?.content;
            const arrayFormulaPosition = this.getArrayFormulaSpreadingOn(position);
            if (arrayFormulaPosition !== undefined) {
                // take into account new collisions.
                impactedRanges.addPosition(arrayFormulaPosition);
            }
            if (!content) {
                // The previous content could have blocked some array formulas
                impactedRanges.addPosition(position);
            }
        }
        for (const range of [...impactedRanges]) {
            impactedRanges.addMany(this.getArrayFormulasBlockedBy(range.sheetId, range.zone));
        }
        return impactedRanges;
    }
    buildDependencyGraph() {
        this.blockedArrayFormulas = this.createEmptyPositionSet();
        this.spreadingRelations = new SpreadingRelation();
        this.formulaDependencies = lazy(() => {
            const rTreeItems = [];
            for (const sheetId of this.getters.getSheetIds()) {
                const cells = this.getters.getCells(sheetId);
                for (const cellId in cells) {
                    const cell = cells[cellId];
                    if (cell.isFormula) {
                        const directDependencies = cell.compiledFormula.dependencies;
                        for (const range of directDependencies) {
                            if (range.invalidSheetName || range.invalidXc) {
                                continue;
                            }
                            rTreeItems.push({
                                data: {
                                    sheetId,
                                    zone: positionToZone(this.getters.getCellPosition(cellId)),
                                },
                                boundingBox: { sheetId: range.sheetId, zone: range.zone },
                            });
                        }
                    }
                }
            }
            return new FormulaDependencyGraph(rTreeItems);
        });
    }
    evaluateAllCells() {
        const start = performance.now();
        this.evaluatedCells = new PositionMap();
        const ranges = [];
        for (const sheetId of this.getters.getSheetIds()) {
            const zone = this.getters.getSheetZone(sheetId);
            ranges.push({ sheetId, zone });
        }
        this.evaluate(ranges);
        console.debug("evaluate all cells", performance.now() - start, "ms");
    }
    evaluateFormulaResult(sheetId, formulaString, originCellPosition) {
        const compiledFormula = compile(formulaString);
        const ranges = compiledFormula.dependencies.map((xc) => this.getters.getRangeFromSheetXC(sheetId, xc));
        this.updateCompilationParametersForIsolatedFormula(originCellPosition);
        return this.evaluateCompiledFormula(sheetId, {
            ...compiledFormula,
            dependencies: ranges,
        });
    }
    evaluateCompiledFormula(sheetId, compiledFormula, getContextualSymbolValue) {
        try {
            const result = updateEvalContextAndExecute(compiledFormula, this.compilationParams, sheetId, this.buildSafeGetSymbolValue(getContextualSymbolValue), this.compilationParams.evalContext.__originCellPosition);
            if (isMatrix(result)) {
                return matrixMap(result, nullValueToZeroValue);
            }
            return nullValueToZeroValue(result);
        }
        catch (error) {
            return handleError(error, "");
        }
    }
    /**
     * Return the position of formulas blocked by the given positions
     * as well as all their dependencies.
     */
    getArrayFormulasBlockedBy(sheetId, zone) {
        const arrayFormulaPositions = new RangeSet();
        const arrayFormulas = this.spreadingRelations.searchFormulaPositionsSpreadingOn(sheetId, zone);
        arrayFormulaPositions.addManyPositions(arrayFormulas);
        const spilledPositions = [...arrayFormulas].filter((position) => !this.blockedArrayFormulas.has(position));
        if (spilledPositions.length) {
            // ignore the formula spreading on the position. Keep only the blocked ones
            arrayFormulaPositions.deleteManyPositions(spilledPositions);
        }
        arrayFormulaPositions.addMany(this.getCellsDependingOn(arrayFormulaPositions));
        return arrayFormulaPositions;
    }
    nextRangesToUpdate = new RangeSet();
    cellsBeingComputed = new Set();
    symbolsBeingComputed = new Set();
    evaluate(ranges) {
        this.cellsBeingComputed = new Set();
        this.nextRangesToUpdate = new RangeSet(ranges);
        let currentIteration = 0;
        while (!this.nextRangesToUpdate.isEmpty() && currentIteration++ < MAX_ITERATION) {
            this.updateCompilationParameters();
            const ranges = [...this.nextRangesToUpdate];
            this.nextRangesToUpdate.clear();
            this.clearEvaluatedRanges(ranges);
            for (const range of ranges) {
                const { left, bottom, right, top } = range.zone;
                for (let col = left; col <= right; col++) {
                    for (let row = top; row <= bottom; row++) {
                        const position = { sheetId: range.sheetId, col, row };
                        if (this.nextRangesToUpdate.hasPosition(position)) {
                            continue;
                        }
                        const evaluatedCell = this.computeCell(position);
                        if (evaluatedCell !== EMPTY_CELL) {
                            this.evaluatedCells.set(position, evaluatedCell);
                        }
                    }
                }
            }
            onIterationEndEvaluationRegistry.getAll().forEach((callback) => callback(this.getters));
        }
        if (currentIteration >= MAX_ITERATION) {
            console.warn("Maximum iteration reached while evaluating cells");
        }
    }
    clearEvaluatedRanges(ranges) {
        for (const range of ranges) {
            const { left, bottom, right, top } = range.zone;
            for (let col = left; col <= right; col++) {
                for (let row = top; row <= bottom; row++) {
                    this.evaluatedCells.delete({ sheetId: range.sheetId, col, row });
                }
            }
        }
    }
    computeCell(position) {
        const evaluation = this.evaluatedCells.get(position);
        if (evaluation) {
            return evaluation; // already computed
        }
        if (!this.blockedArrayFormulas.has(position)) {
            this.invalidateSpreading(position);
        }
        if (this.spreadingRelations.isArrayFormula(position)) {
            this.spreadingRelations.removeNode(position);
        }
        const cell = this.getters.getCell(position);
        if (cell === undefined) {
            return EMPTY_CELL;
        }
        const cellId = cell.id;
        const localeFormat = { format: cell.format, locale: this.getters.getLocale() };
        try {
            if (this.cellsBeingComputed.has(cellId)) {
                return ERROR_CYCLE_CELL;
            }
            this.cellsBeingComputed.add(cellId);
            return cell.isFormula
                ? this.computeFormulaCell(position, cell)
                : evaluateLiteral(cell, localeFormat, position);
        }
        catch (e) {
            e.value = e?.value || CellErrorType.GenericError;
            e.message = e?.message || implementationErrorMessage;
            e.origin = position;
            return createEvaluatedCell(e);
        }
        finally {
            this.cellsBeingComputed.delete(cellId);
        }
    }
    computeAndSave(position) {
        const evaluatedCell = this.computeCell(position);
        if (!this.evaluatedCells.has(position)) {
            this.evaluatedCells.set(position, evaluatedCell);
        }
        return evaluatedCell;
    }
    computeFormulaCell(formulaPosition, cellData) {
        const formulaReturn = updateEvalContextAndExecute(cellData.compiledFormula, this.compilationParams, formulaPosition.sheetId, this.buildSafeGetSymbolValue(), formulaPosition);
        if (!isMatrix(formulaReturn)) {
            const evaluatedCell = createEvaluatedCell(nullValueToZeroValue(formulaReturn), this.getters.getLocale(), cellData, formulaPosition);
            if (evaluatedCell.type === CellValueType.error) {
                evaluatedCell.errorOriginPosition = formulaReturn.errorOriginPosition ?? formulaPosition;
            }
            return evaluatedCell;
        }
        this.assertSheetHasEnoughSpaceToSpreadFormulaResult(formulaPosition, formulaReturn);
        const nbColumns = formulaReturn.length;
        const nbRows = formulaReturn[0].length;
        if (nbRows === 0) {
            // empty matrix
            return createEvaluatedCell({ value: 0 }, this.getters.getLocale(), cellData);
        }
        if (nbRows === 1 && nbColumns === 1) {
            // single value matrix
            return createEvaluatedCell(nullValueToZeroValue(formulaReturn[0][0]), this.getters.getLocale(), cellData);
        }
        const resultZone = {
            top: formulaPosition.row,
            bottom: formulaPosition.row + nbRows - 1,
            left: formulaPosition.col,
            right: formulaPosition.col + nbColumns - 1,
        };
        this.spreadingRelations.addRelation({ resultZone, arrayFormulaPosition: formulaPosition });
        this.assertNoMergedCellsInSpreadZone(formulaPosition, formulaReturn);
        forEachSpreadPositionInMatrix(nbColumns, nbRows, this.checkCollision(formulaPosition));
        forEachSpreadPositionInMatrix(nbColumns, nbRows, 
        // thanks to the isMatrix check above, we know that formulaReturn is MatrixFunctionReturn
        this.spreadValues(formulaPosition, formulaReturn));
        this.invalidatePositionsDependingOnSpread(formulaPosition.sheetId, resultZone);
        return createEvaluatedCell(nullValueToZeroValue(formulaReturn[0][0]), this.getters.getLocale(), cellData);
    }
    invalidatePositionsDependingOnSpread(sheetId, resultZone) {
        // the result matrix is split in 2 zones to exclude the array formula position
        const invalidatedPositions = this.getCellsDependingOn(excludeTopLeft(resultZone).map((zone) => ({ sheetId, zone })));
        invalidatedPositions.delete({ sheetId, zone: resultZone });
        this.nextRangesToUpdate.addMany(invalidatedPositions);
    }
    assertSheetHasEnoughSpaceToSpreadFormulaResult({ sheetId, col, row }, matrixResult) {
        const numberOfCols = this.getters.getNumberCols(sheetId);
        const numberOfRows = this.getters.getNumberRows(sheetId);
        const enoughCols = col + matrixResult.length <= numberOfCols;
        const enoughRows = row + matrixResult[0].length <= numberOfRows;
        if (enoughCols && enoughRows) {
            return;
        }
        if (enoughCols) {
            throw new SplillBlockedError(_t("Result couldn't be automatically expanded. Please insert more rows."));
        }
        if (enoughRows) {
            throw new SplillBlockedError(_t("Result couldn't be automatically expanded. Please insert more columns."));
        }
        throw new SplillBlockedError(_t("Result couldn't be automatically expanded. Please insert more columns and rows."));
    }
    assertNoMergedCellsInSpreadZone({ sheetId, col, row }, matrixResult) {
        const mergedCells = this.getters.getMergesInZone(sheetId, {
            top: row,
            bottom: row + matrixResult[0].length - 1,
            left: col,
            right: col + matrixResult.length - 1,
        });
        if (mergedCells.length === 0) {
            return;
        }
        throw new SplillBlockedError(_t("Merged cells found in the spill zone. Please unmerge cells before using array formulas."));
    }
    checkCollision(formulaPosition) {
        const { sheetId, col, row } = formulaPosition;
        const checkCollision = (i, j) => {
            const position = { sheetId: sheetId, col: i + col, row: j + row };
            const rawCell = this.getters.getCell(position);
            if (rawCell?.content ||
                this.getters.getEvaluatedCell(position).type !== CellValueType.empty) {
                this.blockedArrayFormulas.add(formulaPosition);
                throw new SplillBlockedError(_t("Array result was not expanded because it would overwrite data in %s.", toXC(position.col, position.row)));
            }
            this.blockedArrayFormulas.delete(formulaPosition);
        };
        return checkCollision;
    }
    spreadValues({ sheetId, col, row }, matrixResult) {
        const spreadValues = (i, j) => {
            const position = { sheetId, col: i + col, row: j + row };
            const cell = this.getters.getCell(position);
            const evaluatedCell = createEvaluatedCell(nullValueToZeroValue(matrixResult[i][j]), this.getters.getLocale(), cell, position);
            if (evaluatedCell.type === CellValueType.error) {
                evaluatedCell.errorOriginPosition = matrixResult[i][j].errorOriginPosition ?? position;
            }
            this.evaluatedCells.set(position, evaluatedCell);
        };
        return spreadValues;
    }
    invalidateSpreading(position) {
        const zone = this.spreadingRelations.getArrayResultZone(position);
        if (!zone) {
            return;
        }
        for (let col = zone.left; col <= zone.right; col++) {
            for (let row = zone.top; row <= zone.bottom; row++) {
                const resultPosition = { sheetId: position.sheetId, col, row };
                const content = this.getters.getCell(resultPosition)?.content;
                if (content) {
                    // there's no point at re-evaluating overlapping array formulas,
                    // there's still a collision
                    continue;
                }
                this.evaluatedCells.delete(resultPosition);
            }
        }
        const sheetId = position.sheetId;
        this.invalidatePositionsDependingOnSpread(sheetId, zone);
        this.nextRangesToUpdate.addMany(this.getArrayFormulasBlockedBy(sheetId, zone));
    }
    /**
     * Wraps a GetSymbolValue function to add cycle detection
     * and error handling.
     */
    buildSafeGetSymbolValue(getContextualSymbolValue) {
        const getSymbolValue = (symbolName) => {
            if (this.symbolsBeingComputed.has(symbolName)) {
                return ERROR_CYCLE_CELL;
            }
            this.symbolsBeingComputed.add(symbolName);
            try {
                const symbolValue = getContextualSymbolValue?.(symbolName);
                if (symbolValue) {
                    return symbolValue;
                }
                return new BadExpressionError(_t("Invalid formula"));
            }
            finally {
                this.symbolsBeingComputed.delete(symbolName);
            }
        };
        return getSymbolValue;
    }
    // ----------------------------------------------------------
    //                 COMMON FUNCTIONALITY
    // ----------------------------------------------------------
    getDirectDependencies(position) {
        const cell = this.getters.getCell(position);
        if (!cell?.isFormula) {
            return [];
        }
        return cell.compiledFormula.dependencies;
    }
    getCellsDependingOn(ranges) {
        return this.formulaDependencies().getCellsDependingOn(ranges, this.nextRangesToUpdate);
    }
}
function forEachSpreadPositionInMatrix(nbColumns, nbRows, callback) {
    for (let i = 0; i < nbColumns; ++i) {
        for (let j = 0; j < nbRows; ++j) {
            if (i === 0 && j === 0) {
                continue;
            }
            callback(i, j);
        }
    }
}
/**
 * This function replaces null payload values with 0.
 * This aids in the UI by ensuring that a cell with a
 * formula referencing an empty cell displays a value (0),
 * rather than appearing empty. This indicates that the
 * cell is the result of a non-empty content.
 */
function nullValueToZeroValue(functionResult) {
    if (functionResult.value === null || functionResult.value === undefined) {
        // 'functionResult.value === undefined' is supposed to never happen, it's a safety net for javascript use
        return { ...functionResult, value: 0 };
    }
    return functionResult;
}
function updateEvalContextAndExecute(compiledFormula, compilationParams, sheetId, getSymbolValue, originCellPosition) {
    const evalContext = compilationParams.evalContext;
    const currentCellPosition = evalContext.__originCellPosition;
    const currentSheetId = evalContext.__originSheetId;
    evalContext.__originCellPosition = originCellPosition;
    evalContext.__originSheetId = sheetId;
    const result = compiledFormula.execute(compiledFormula.dependencies, compilationParams.referenceDenormalizer, compilationParams.ensureRange, getSymbolValue, evalContext);
    evalContext.__originCellPosition = currentCellPosition;
    evalContext.__originSheetId = currentSheetId;
    return result;
}

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

const chartColorRegex = /"(#[0-9a-fA-F]{6})"/g;
/**
 * https://tomekdev.com/posts/sorting-colors-in-js
 */
function sortWithClusters(colorsToSort) {
    const clusters = [
        { leadColor: rgba(255, 0, 0), colors: [] }, // red
        { leadColor: rgba(255, 128, 0), colors: [] }, // orange
        { leadColor: rgba(128, 128, 0), colors: [] }, // yellow
        { leadColor: rgba(128, 255, 0), colors: [] }, // chartreuse
        { leadColor: rgba(0, 255, 0), colors: [] }, // green
        { leadColor: rgba(0, 255, 128), colors: [] }, // spring green
        { leadColor: rgba(0, 255, 255), colors: [] }, // cyan
        { leadColor: rgba(0, 127, 255), colors: [] }, // azure
        { leadColor: rgba(0, 0, 255), colors: [] }, // blue
        { leadColor: rgba(127, 0, 255), colors: [] }, // violet
        { leadColor: rgba(128, 0, 128), colors: [] }, // magenta
        { 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 CoreViewPlugin {
    customColors = {};
    shouldUpdateColors = true;
    static getters = ["getCustomColors"];
    constructor(config) {
        super(config);
        this.tryToAddColors(config.customColors ?? []);
    }
    handle(cmd) {
        switch (cmd.type) {
            case "START":
                for (const sheetId of this.getters.getSheetIds()) {
                    for (const chartId of this.getters.getChartIds(sheetId)) {
                        this.tryToAddColors(this.getChartColors(chartId));
                    }
                }
                break;
            case "UPDATE_CHART":
            case "CREATE_CHART":
                this.tryToAddColors(this.getChartColors(cmd.chartId));
                break;
            case "UPDATE_CELL":
            case "ADD_CONDITIONAL_FORMAT":
            case "SET_BORDER":
            case "SET_ZONE_BORDERS":
            case "SET_FORMATTING":
            case "CREATE_TABLE":
            case "UPDATE_TABLE":
                this.history.update("shouldUpdateColors", true);
                break;
        }
    }
    finalize() {
        if (this.shouldUpdateColors) {
            this.history.update("shouldUpdateColors", false);
            this.tryToAddColors(this.computeCustomColors());
        }
    }
    getCustomColors() {
        return sortWithClusters(Object.keys(this.customColors));
    }
    computeCustomColors() {
        let usedColors = [];
        for (const sheetId of this.getters.getSheetIds()) {
            usedColors = usedColors.concat(this.getColorsFromCells(sheetId), this.getFormattingColors(sheetId), this.getTableColors(sheetId));
        }
        return [...new Set([...usedColors])];
    }
    getColorsFromCells(sheetId) {
        const cells = Object.values(this.getters.getCells(sheetId));
        const colors = new Set();
        for (const cell of cells) {
            if (cell.style?.textColor) {
                colors.add(cell.style.textColor);
            }
            if (cell.style?.fillColor) {
                colors.add(cell.style.fillColor);
            }
        }
        for (const color of this.getters.getBordersColors(sheetId)) {
            colors.add(color);
        }
        return [...colors];
    }
    getFormattingColors(sheetId) {
        const formats = this.getters.getConditionalFormats(sheetId);
        const formatColors = [];
        for (const format of formats) {
            const rule = format.rule;
            if (rule.type === "CellIsRule") {
                formatColors.push(rule.style.textColor);
                formatColors.push(rule.style.fillColor);
            }
            else if (rule.type === "ColorScaleRule") {
                formatColors.push(colorNumberToHex(rule.minimum.color));
                formatColors.push(rule.midpoint ? colorNumberToHex(rule.midpoint.color) : undefined);
                formatColors.push(colorNumberToHex(rule.maximum.color));
            }
        }
        return formatColors.filter(isDefined);
    }
    getChartColors(chartId) {
        const chart = this.getters.getChart(chartId);
        if (chart === undefined) {
            return [];
        }
        const stringifiedChart = JSON.stringify(chart.getDefinition());
        const colors = stringifiedChart.matchAll(chartColorRegex);
        return [...colors].map((color) => color[1]); // color[1] is the first capturing group of the regex
    }
    getTableColors(sheetId) {
        const tables = this.getters.getTables(sheetId);
        return tables.flatMap((table) => {
            const config = table.config;
            const style = this.getters.getTableStyle(config.styleId);
            return [
                this.getTableStyleElementColors(style.wholeTable),
                config.numberOfHeaders > 0 ? this.getTableStyleElementColors(style.headerRow) : [],
                config.totalRow ? this.getTableStyleElementColors(style.totalRow) : [],
                config.bandedColumns ? this.getTableStyleElementColors(style.firstColumnStripe) : [],
                config.bandedColumns ? this.getTableStyleElementColors(style.secondColumnStripe) : [],
                config.bandedRows ? this.getTableStyleElementColors(style.firstRowStripe) : [],
                config.bandedRows ? this.getTableStyleElementColors(style.secondRowStripe) : [],
                config.firstColumn ? this.getTableStyleElementColors(style.firstColumn) : [],
                config.lastColumn ? this.getTableStyleElementColors(style.lastColumn) : [],
            ].flat();
        });
    }
    getTableStyleElementColors(element) {
        if (!element) {
            return [];
        }
        return [
            element.style?.fillColor,
            element.style?.textColor,
            element.border?.bottom?.color,
            element.border?.top?.color,
            element.border?.left?.color,
            element.border?.right?.color,
            element.border?.horizontal?.color,
            element.border?.vertical?.color,
        ].filter(isDefined);
    }
    tryToAddColors(colors) {
        for (const color of colors) {
            if (!isColorValid(color)) {
                continue;
            }
            const formattedColor = toHex(color);
            if (color && !COLOR_PICKER_DEFAULTS.includes(formattedColor)) {
                this.history.update("customColors", formattedColor, true);
            }
        }
    }
}

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

class EvaluationConditionalFormatPlugin extends CoreViewPlugin {
    static getters = [
        "getConditionalIcon",
        "getCellConditionalFormatStyle",
        "getConditionalDataBar",
    ];
    isStale = true;
    // stores the computed styles in the format of computedStyles.sheetName[col][row] = Style
    computedStyles = {};
    computedIcons = {};
    computedDataBars = {};
    // ---------------------------------------------------------------------------
    // Command Handling
    // ---------------------------------------------------------------------------
    handle(cmd) {
        if (invalidateEvaluationCommands.has(cmd.type) ||
            invalidateCFEvaluationCommands.has(cmd.type) ||
            (cmd.type === "UPDATE_CELL" && ("content" in cmd || "format" in cmd))) {
            this.isStale = true;
        }
    }
    finalize() {
        if (this.isStale) {
            for (const sheetId of this.getters.getSheetIds()) {
                this.computedStyles[sheetId] = lazy(() => this.getComputedStyles(sheetId));
                this.computedIcons[sheetId] = lazy(() => this.getComputedIcons(sheetId));
                this.computedDataBars[sheetId] = lazy(() => this.getComputedDataBars(sheetId));
            }
            this.isStale = false;
        }
    }
    // ---------------------------------------------------------------------------
    // Getters
    // ---------------------------------------------------------------------------
    getCellConditionalFormatStyle(position) {
        const { sheetId, col, row } = position;
        const styles = this.computedStyles[sheetId]();
        return styles && styles[col]?.[row];
    }
    getConditionalIcon({ sheetId, col, row }) {
        const icons = this.computedIcons[sheetId]();
        return icons && icons[col]?.[row];
    }
    getConditionalDataBar({ sheetId, col, row }) {
        const dataBars = this.computedDataBars[sheetId]();
        return dataBars && dataBars[col]?.[row];
    }
    // ---------------------------------------------------------------------------
    // Private
    // ---------------------------------------------------------------------------
    /**
     * Compute the styles according to the conditional formatting.
     * This computation must happen after the cell values are computed if they change
     *
     * This result of the computation will be in the state.cell[XC].conditionalStyle and will be the union of all the style
     * properties of the rules applied (in order).
     * So if a cell has multiple conditional formatting applied to it, and each affect a different value of the style,
     * the resulting style will have the combination of all those values.
     * If multiple conditional formatting use the same style value, they will be applied in order so that the last applied wins
     */
    getComputedStyles(sheetId) {
        const computedStyle = {};
        for (const cf of this.getters.getConditionalFormats(sheetId).reverse()) {
            switch (cf.rule.type) {
                case "ColorScaleRule":
                    for (const range of cf.ranges) {
                        this.applyColorScale(sheetId, range, cf.rule, computedStyle);
                    }
                    break;
                case "CellIsRule":
                    const formulas = cf.rule.values.map((value) => value.startsWith("=") ? compile(value) : undefined);
                    for (const ref of cf.ranges) {
                        const zone = this.getters.getRangeFromSheetXC(sheetId, ref).zone;
                        for (let row = zone.top; row <= zone.bottom; row++) {
                            for (let col = zone.left; col <= zone.right; col++) {
                                const target = { sheetId, col, row };
                                const values = cf.rule.values.map((value, i) => {
                                    const compiledFormula = formulas[i];
                                    if (compiledFormula) {
                                        return this.getters.getTranslatedCellFormula(sheetId, col - zone.left, row - zone.top, compiledFormula.tokens);
                                    }
                                    return value;
                                });
                                if (this.getRuleResultForTarget(target, { ...cf.rule, values })) {
                                    if (!computedStyle[col])
                                        computedStyle[col] = [];
                                    // we must combine all the properties of all the CF rules applied to the given cell
                                    computedStyle[col][row] = Object.assign(computedStyle[col]?.[row] || {}, cf.rule.style);
                                }
                            }
                        }
                    }
                    break;
            }
        }
        return computedStyle;
    }
    getComputedIcons(sheetId) {
        const computedIcons = {};
        for (const cf of this.getters.getConditionalFormats(sheetId).reverse()) {
            if (cf.rule.type !== "IconSetRule")
                continue;
            for (const range of cf.ranges) {
                this.applyIcon(sheetId, range, cf.rule, computedIcons);
            }
        }
        return computedIcons;
    }
    getComputedDataBars(sheetId) {
        const computedDataBars = {};
        for (const cf of this.getters.getConditionalFormats(sheetId).reverse()) {
            if (cf.rule.type !== "DataBarRule")
                continue;
            for (const range of cf.ranges) {
                this.applyDataBar(sheetId, range, cf.rule, computedDataBars);
            }
        }
        return computedDataBars;
    }
    parsePoint(sheetId, range, threshold, functionName) {
        const rangeValues = this.getters
            .getEvaluatedCellsInZone(sheetId, this.getters.getRangeFromSheetXC(sheetId, range).zone)
            .filter((cell) => cell.type === CellValueType.number)
            .map((cell) => cell.value);
        switch (threshold.type) {
            case "value":
                return functionName === "max" ? largeMax(rangeValues) : largeMin(rangeValues);
            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(sheetId, threshold.value);
                return typeof value === "number" ? value : null;
            default:
                return null;
        }
    }
    /** Compute the CF icons for the given range and CF rule, and apply in in the given computedIcons object */
    applyIcon(sheetId, range, rule, computedIcons) {
        const lowerInflectionPoint = this.parsePoint(sheetId, range, rule.lowerInflectionPoint);
        const upperInflectionPoint = this.parsePoint(sheetId, range, rule.upperInflectionPoint);
        if (lowerInflectionPoint === null ||
            upperInflectionPoint === null ||
            lowerInflectionPoint > upperInflectionPoint) {
            return;
        }
        const zone = this.getters.getRangeFromSheetXC(sheetId, range).zone;
        const iconSet = [rule.icons.upper, rule.icons.middle, rule.icons.lower];
        for (let row = zone.top; row <= zone.bottom; row++) {
            for (let col = zone.left; col <= zone.right; col++) {
                const cell = this.getters.getEvaluatedCell({ sheetId, col, row });
                if (cell.type !== CellValueType.number) {
                    continue;
                }
                const icon = this.computeIcon(cell.value, upperInflectionPoint, rule.upperInflectionPoint.operator, lowerInflectionPoint, rule.lowerInflectionPoint.operator, iconSet);
                if (!computedIcons[col]) {
                    computedIcons[col] = [];
                }
                computedIcons[col][row] = icon;
            }
        }
    }
    computeIcon(value, upperInflectionPoint, upperOperator, lowerInflectionPoint, lowerOperator, icons) {
        if ((upperOperator === "ge" && value >= upperInflectionPoint) ||
            (upperOperator === "gt" && value > upperInflectionPoint)) {
            return icons[0];
        }
        else if ((lowerOperator === "ge" && value >= lowerInflectionPoint) ||
            (lowerOperator === "gt" && value > lowerInflectionPoint)) {
            return icons[1];
        }
        return icons[2];
    }
    applyDataBar(sheetId, range, rule, computedDataBars) {
        const rangeValues = this.getters.getRangeFromSheetXC(sheetId, rule.rangeValues || range);
        const allValues = this.getters
            .getEvaluatedCellsInZone(sheetId, rangeValues.zone)
            .filter((cell) => cell.type === CellValueType.number)
            .map((cell) => cell.value);
        const max = largeMax(allValues);
        if (max <= 0) {
            // no need to apply the data bar if all values are negative or 0
            return;
        }
        const color = rule.color;
        const zone = this.getters.getRangeFromSheetXC(sheetId, range).zone;
        const zoneOfValues = rangeValues.zone;
        for (let row = zone.top; row <= zone.bottom; row++) {
            for (let col = zone.left; col <= zone.right; col++) {
                const targetCol = col - zone.left + zoneOfValues.left;
                const targetRow = row - zone.top + zoneOfValues.top;
                const cell = this.getters.getEvaluatedCell({ sheetId, col: targetCol, row: targetRow });
                if (!isInside(targetCol, targetRow, zoneOfValues) ||
                    cell.type !== CellValueType.number ||
                    cell.value <= 0) {
                    // values negatives or 0 are ignored
                    continue;
                }
                if (!computedDataBars[col])
                    computedDataBars[col] = [];
                computedDataBars[col][row] = {
                    color: colorNumberToHex(color),
                    percentage: (cell.value * 100) / max,
                };
            }
        }
    }
    /** Compute the color scale for the given range and CF rule, and apply in in the given computedStyle object */
    applyColorScale(sheetId, range, rule, computedStyle) {
        const minValue = this.parsePoint(sheetId, range, rule.minimum, "min");
        const midValue = rule.midpoint
            ? this.parsePoint(sheetId, range, rule.midpoint)
            : null;
        const maxValue = this.parsePoint(sheetId, range, rule.maximum, "max");
        if (minValue === null ||
            maxValue === null ||
            minValue >= maxValue ||
            (midValue && (minValue >= midValue || midValue >= maxValue))) {
            return;
        }
        const zone = this.getters.getRangeFromSheetXC(sheetId, range).zone;
        const colorThresholds = [{ value: minValue, color: rule.minimum.color }];
        if (rule.midpoint && midValue) {
            colorThresholds.push({ value: midValue, color: rule.midpoint.color });
        }
        colorThresholds.push({ value: maxValue, color: rule.maximum.color });
        const colorScale = getColorScale(colorThresholds);
        for (let row = zone.top; row <= zone.bottom; row++) {
            for (let col = zone.left; col <= zone.right; col++) {
                const cell = this.getters.getEvaluatedCell({ sheetId, col, row });
                if (cell.type === CellValueType.number) {
                    const value = clip(cell.value, minValue, maxValue);
                    if (!computedStyle[col])
                        computedStyle[col] = [];
                    computedStyle[col][row] = computedStyle[col]?.[row] || {};
                    computedStyle[col][row].fillColor = colorScale(value);
                }
            }
        }
    }
    getRuleResultForTarget(target, rule) {
        const cell = this.getters.getEvaluatedCell(target);
        if (cell.type === CellValueType.error) {
            return false;
        }
        const { sheetId } = target;
        const evaluator = criterionEvaluatorRegistry.get(rule.operator);
        const evaluatedCriterionValues = rule.values.map((value) => {
            if (!value.startsWith("=")) {
                return parseLiteral(value, DEFAULT_LOCALE);
            }
            return this.getters.evaluateFormula(sheetId, value) ?? "";
        });
        if (evaluatedCriterionValues.some(isMultipleElementMatrix)) {
            return false;
        }
        const evaluatedCriterion = {
            type: rule.operator,
            values: evaluatedCriterionValues.map(toScalar),
        };
        return evaluator.isValueValid(cell.value ?? "", evaluatedCriterion, this.getters, sheetId);
    }
}

const VALID_RESULT = { isValid: true };
class EvaluationDataValidationPlugin extends CoreViewPlugin {
    static getters = [
        "getDataValidationInvalidCriterionValueMessage",
        "getInvalidDataValidationMessage",
        "getValidationResultForCellValue",
        "getDataValidationRangeValues",
        "isCellValidCheckbox",
        "getDataValidationCellStyle",
        "getDataValidationChipStyle",
        "isDataValidationInvalid",
    ];
    validationResults = {};
    handle(cmd) {
        if (invalidateEvaluationCommands.has(cmd.type) ||
            cmd.type === "EVALUATE_CELLS" ||
            (cmd.type === "UPDATE_CELL" && ("content" in cmd || "format" in cmd))) {
            this.validationResults = {};
            return;
        }
        switch (cmd.type) {
            case "ADD_DATA_VALIDATION_RULE":
            case "REMOVE_DATA_VALIDATION_RULE":
                delete this.validationResults[cmd.sheetId];
                break;
        }
    }
    isDataValidationInvalid(cellPosition) {
        return !this.getValidationResultForCell(cellPosition).isValid;
    }
    getDataValidationCellStyle(position) {
        if (this.hasChip(position)) {
            return undefined; // The style is not applied on the cell if it's a chip
        }
        return this.getDataValidationStyle(position);
    }
    getDataValidationChipStyle(position) {
        if (this.hasChip(position)) {
            return this.getDataValidationStyle(position) ?? { fillColor: GRAY_200 };
        }
        return undefined;
    }
    getInvalidDataValidationMessage(cellPosition) {
        const validationResult = this.getValidationResultForCell(cellPosition);
        return validationResult.isValid ? undefined : validationResult.error;
    }
    /**
     * Check if the value is valid for the given criterion, and return an error message if not.
     *
     * The value must be canonicalized.
     */
    getDataValidationInvalidCriterionValueMessage(criterionType, value) {
        const evaluator = criterionEvaluatorRegistry.get(criterionType);
        if (value.startsWith("=")) {
            if (evaluator.allowedValues === "onlyLiterals") {
                return _t("The value must not be a formula");
            }
            return this.isValidFormula(value) ? undefined : DVTerms.CriterionError.validFormula;
        }
        else if (evaluator.allowedValues === "onlyFormulas") {
            return _t("The value must be a formula");
        }
        return evaluator.isCriterionValueValid(value) ? undefined : evaluator.criterionValueErrorString;
    }
    getDataValidationRangeValues(sheetId, criterion) {
        const range = this.getters.getRangeFromSheetXC(sheetId, String(criterion.values[0]));
        const values = [];
        const labelsSet = new Set();
        for (const p of positions(range.zone)) {
            const cell = this.getters.getEvaluatedCell({ ...p, sheetId: range.sheetId });
            if (cell.formattedValue && !labelsSet.has(cell.formattedValue)) {
                labelsSet.add(cell.formattedValue);
                values.push({ label: cell.formattedValue, value: cell.value?.toString() || "" });
            }
        }
        return values;
    }
    isCellValidCheckbox(cellPosition) {
        if (!this.getters.isMainCellPosition(cellPosition)) {
            return false;
        }
        const rule = this.getters.getValidationRuleForCell(cellPosition);
        if (!rule || rule.criterion.type !== "isBoolean") {
            return false;
        }
        return this.getValidationResultForCell(cellPosition).isValid;
    }
    /** Get the validation result if the cell on the given position had the given value */
    getValidationResultForCellValue(cellValue, cellPosition) {
        const rule = this.getters.getValidationRuleForCell(cellPosition);
        if (!rule) {
            return VALID_RESULT;
        }
        const error = this.getRuleErrorForCellValue(cellValue, cellPosition, rule);
        return error ? { error, rule, isValid: false } : VALID_RESULT;
    }
    hasChip(position) {
        const rule = this.getters.getValidationRuleForCell(position);
        return ((rule?.criterion.type === "isValueInList" || rule?.criterion.type === "isValueInRange") &&
            rule.criterion.displayStyle === "chip");
    }
    getDataValidationStyle(position) {
        const rule = this.getters.getValidationRuleForCell(position);
        if (!rule || this.isDataValidationInvalid(position)) {
            return undefined;
        }
        const evaluatedCell = this.getters.getEvaluatedCell(position);
        const color = this.getValueColor(rule, evaluatedCell.value);
        if (!color) {
            return undefined;
        }
        const style = {
            fillColor: color,
            textColor: chipTextColor(color),
        };
        return style;
    }
    getValueColor(rule, value) {
        if (rule.criterion.type !== "isValueInList" && rule.criterion.type !== "isValueInRange") {
            return undefined;
        }
        for (const criterionValue in rule.criterion.colors) {
            if (criterionValue.toLowerCase() === String(value).toLowerCase()) {
                return rule.criterion.colors[criterionValue];
            }
        }
        return undefined;
    }
    isValidFormula(value) {
        return !compile(value).isBadExpression;
    }
    getValidationResultForCell(cellPosition) {
        const { col, row, sheetId } = cellPosition;
        if (!this.validationResults[sheetId]) {
            this.validationResults[sheetId] = this.computeSheetValidationResults(sheetId);
        }
        return this.validationResults[sheetId][col]?.[row]?.() || VALID_RESULT;
    }
    computeSheetValidationResults(sheetId) {
        const validationResults = {};
        const ranges = this.getters.getDataValidationRules(sheetId).map((rule) => rule.ranges);
        for (const cellPosition of getCellPositionsInRanges(ranges.flat())) {
            const { col, row } = cellPosition;
            if (!validationResults[col]) {
                validationResults[col] = [];
            }
            validationResults[col][row] = lazy(() => {
                const evaluatedCell = this.getters.getEvaluatedCell(cellPosition);
                if (evaluatedCell.type === CellValueType.empty) {
                    return VALID_RESULT;
                }
                return this.getValidationResultForCellValue(evaluatedCell.value, cellPosition);
            });
        }
        return validationResults;
    }
    getRuleErrorForCellValue(cellValue, cellPosition, rule) {
        const { sheetId } = cellPosition;
        const criterion = rule.criterion;
        const evaluator = criterionEvaluatorRegistry.get(criterion.type);
        const offset = this.getCellOffsetInRule(cellPosition, rule);
        const evaluatedCriterionValues = this.getEvaluatedCriterionValues(sheetId, offset, criterion);
        if (evaluatedCriterionValues.some(isMultipleElementMatrix)) {
            return undefined;
        }
        const evaluatedCriterion = { ...criterion, values: evaluatedCriterionValues.map(toScalar) };
        if (evaluator.isValueValid(cellValue, evaluatedCriterion, this.getters, sheetId)) {
            return undefined;
        }
        return evaluator.getErrorString(evaluatedCriterion, this.getters, sheetId);
    }
    /** Get the offset of the cell inside the ranges of the rule. Throws an error if the cell isn't inside the rule. */
    getCellOffsetInRule(cellPosition, rule) {
        const range = rule.ranges.find((range) => isInside(cellPosition.col, cellPosition.row, range.zone));
        if (!range) {
            throw new Error("The cell is not in any range of the rule");
        }
        return {
            col: cellPosition.col - range.zone.left,
            row: cellPosition.row - range.zone.top,
        };
    }
    getEvaluatedCriterionValues(sheetId, offset, criterion) {
        return criterion.values.map((value) => {
            if (!value.startsWith("=")) {
                return parseLiteral(value, DEFAULT_LOCALE);
            }
            const formula = compile(value);
            const translatedFormula = this.getters.getTranslatedCellFormula(sheetId, offset.col, offset.row, formula.tokens);
            return this.getters.evaluateFormula(sheetId, translatedFormula);
        });
    }
}

/**
 * Registry to draw icons on cells
 */
const iconsOnCellRegistry = new Registry();
iconsOnCellRegistry.add("data_validation_checkbox", (getters, position) => {
    const hasIcon = getters.isCellValidCheckbox(position);
    if (hasIcon) {
        const value = !!getters.getEvaluatedCell(position).value;
        return {
            svg: value ? CHECKBOX_CHECKED : CHECKBOX_UNCHECKED,
            hoverSvg: value ? CHECKBOX_CHECKED : CHECKBOX_UNCHECKED_HOVERED,
            priority: 2,
            horizontalAlign: "center",
            size: GRID_ICON_EDGE_LENGTH,
            margin: GRID_ICON_MARGIN,
            position,
            type: "data_validation_checkbox",
            onClick: (position, env) => {
                const cell = env.model.getters.getCell(position);
                const isDisabled = env.model.getters.isReadonly() || !!cell?.isFormula;
                if (isDisabled) {
                    return;
                }
                const cellContent = value ? "FALSE" : "TRUE";
                env.model.dispatch("UPDATE_CELL", { ...position, content: cellContent });
            },
        };
    }
    return undefined;
});
iconsOnCellRegistry.add("data_validation_chip_icon", (getters, position) => {
    const chipStyle = getters.getDataValidationChipStyle(position);
    if (chipStyle) {
        const cellStyle = getters.getCellComputedStyle(position);
        return {
            svg: getChipSvg(chipStyle),
            hoverSvg: getHoveredChipSvg(chipStyle),
            priority: 10,
            horizontalAlign: "right",
            size: computeTextFontSizeInPixels(cellStyle),
            margin: 4,
            position,
            onClick: (position, env) => {
                const { col, row } = position;
                env.model.selection.selectCell(col, row);
                env.startCellEdition();
            },
            type: "data_validation_chip_icon",
        };
    }
    return undefined;
});
iconsOnCellRegistry.add("data_validation_list_icon", (getters, position) => {
    const hasIcon = !getters.isReadonly() && getters.cellHasListDataValidationIcon(position);
    if (hasIcon) {
        const cellStyle = getters.getCellComputedStyle(position);
        return {
            svg: getCaretDownSvg(cellStyle),
            hoverSvg: getHoveredCaretDownSvg(cellStyle),
            priority: 2,
            horizontalAlign: "right",
            size: GRID_ICON_EDGE_LENGTH,
            margin: GRID_ICON_MARGIN,
            position,
            onClick: (position, env) => {
                const { col, row } = position;
                env.model.selection.selectCell(col, row);
                env.startCellEdition();
            },
            type: "data_validation_list_icon",
        };
    }
    return undefined;
});
iconsOnCellRegistry.add("filter_icon", (getters, position) => {
    const hasIcon = getters.isFilterHeader(position);
    if (hasIcon) {
        const isFilterActive = getters.isFilterActive(position);
        const cellStyle = getters.getCellComputedStyle(position);
        const isHighContrast = relativeLuminance(cellStyle.fillColor || "#fff") < 0.45;
        return {
            type: "filter_icon",
            svg: getDataFilterIcon(isFilterActive, isHighContrast, false),
            hoverSvg: getDataFilterIcon(isFilterActive, isHighContrast, true),
            priority: 3,
            horizontalAlign: "right",
            size: GRID_ICON_EDGE_LENGTH,
            margin: GRID_ICON_MARGIN,
            position,
            onClick: (position, env) => {
                const cellPopovers = env.getStore(CellPopoverStore);
                const activePopover = cellPopovers.persistentCellPopover;
                if (activePopover.isOpen &&
                    activePopover.col === position.col &&
                    activePopover.row === position.row &&
                    activePopover.type === "FilterMenu") {
                    cellPopovers.close();
                    return;
                }
                cellPopovers.open(position, "FilterMenu");
            },
        };
    }
    return undefined;
});
iconsOnCellRegistry.add("conditional_formatting", (getters, position) => {
    const icon = getters.getConditionalIcon(position);
    if (icon) {
        const style = getters.getCellStyle(position);
        return {
            type: "conditional_formatting",
            svg: ICONS[icon].svg,
            priority: 1,
            horizontalAlign: "left",
            size: computeTextFontSizeInPixels(style),
            margin: MIN_CF_ICON_MARGIN,
            position,
        };
    }
    return undefined;
});
iconsOnCellRegistry.add("pivot_collapse", (getters, position) => {
    if (!getters.isSpillPivotFormula(position)) {
        return undefined;
    }
    const pivotCell = getters.getPivotCellFromPosition(position);
    const pivotId = getters.getPivotIdFromPosition(position);
    if (pivotCell.type === "HEADER" && pivotId && pivotCell.domain.length) {
        const definition = getters.getPivotCoreDefinition(pivotId);
        const isDashboard = getters.isDashboard();
        const fields = pivotCell.dimension === "COL" ? definition.columns : definition.rows;
        const hasIcon = !isDashboard && pivotCell.domain.length !== fields.length;
        const domains = definition.collapsedDomains?.[pivotCell.dimension] ?? [];
        const isCollapsed = domains.some((domain) => deepEquals(domain, pivotCell.domain));
        const indent = pivotCell.dimension === "ROW" ? (pivotCell.domain.length - 1) * PIVOT_INDENT : 0;
        return {
            type: "pivot_collapse",
            priority: 4,
            horizontalAlign: "left",
            size: hasIcon || (!isDashboard && pivotCell.dimension === "ROW" && definition.rows.length > 1)
                ? PIVOT_COLLAPSE_ICON_SIZE
                : 0,
            margin: hasIcon ? GRID_ICON_MARGIN * 2 + indent : indent,
            svg: hasIcon ? getPivotIconSvg(isCollapsed, false) : undefined,
            hoverSvg: hasIcon ? getPivotIconSvg(isCollapsed, true) : undefined,
            position,
            onClick: togglePivotCollapse,
        };
    }
    return undefined;
});
function togglePivotCollapse(position, env) {
    const pivotCell = env.model.getters.getPivotCellFromPosition(position);
    const pivotId = env.model.getters.getPivotIdFromPosition(position);
    if (!pivotId || pivotCell.type !== "HEADER") {
        return;
    }
    const definition = env.model.getters.getPivotCoreDefinition(pivotId);
    const collapsedDomains = definition.collapsedDomains?.[pivotCell.dimension]
        ? [...definition.collapsedDomains[pivotCell.dimension]]
        : [];
    const index = collapsedDomains.findIndex((domain) => deepEquals(domain, pivotCell.domain));
    if (index !== -1) {
        collapsedDomains.splice(index, 1);
    }
    else {
        collapsedDomains.push(pivotCell.domain);
    }
    const newDomains = definition.collapsedDomains
        ? { ...definition.collapsedDomains }
        : { COL: [], ROW: [] };
    newDomains[pivotCell.dimension] = collapsedDomains;
    env.model.dispatch("UPDATE_PIVOT", {
        pivotId,
        pivot: { ...definition, collapsedDomains: newDomains },
    });
}
iconsOnCellRegistry.add("pivot_dashboard_sorting", (getters, position) => {
    if (!getters.isDashboard()) {
        return undefined;
    }
    const pivotCell = getters.getPivotCellFromPosition(position);
    if (pivotCell.type !== "MEASURE_HEADER") {
        return undefined;
    }
    const sortDirection = getters.getPivotCellSortDirection(position);
    if (sortDirection !== "asc" && sortDirection !== "desc") {
        return undefined;
    }
    const cellStyle = getters.getCellComputedStyle(position);
    return {
        type: `pivot_dashboard_sorting_${sortDirection}`,
        priority: 5,
        horizontalAlign: "right",
        size: GRID_ICON_EDGE_LENGTH,
        margin: GRID_ICON_MARGIN,
        svg: sortDirection === "asc" ? getCaretUpSvg(cellStyle) : getCaretDownSvg(cellStyle),
        position,
        onClick: undefined, // click is managed by ClickableCellSortIcon
    };
});

class CellIconPlugin extends CoreViewPlugin {
    static getters = ["doesCellHaveGridIcon", "getCellIcons", "getCellIconRect"];
    cellIconsCache = {};
    handle(cmd) {
        if (cmd.type !== "SET_VIEWPORT_OFFSET") {
            this.cellIconsCache = {};
        }
    }
    getCellIcons(position) {
        if (!this.cellIconsCache[position.sheetId]) {
            this.cellIconsCache[position.sheetId] = {};
        }
        if (!this.cellIconsCache[position.sheetId][position.col]) {
            this.cellIconsCache[position.sheetId][position.col] = {};
        }
        if (!this.cellIconsCache[position.sheetId][position.col][position.row]) {
            this.cellIconsCache[position.sheetId][position.col][position.row] =
                this.computeCellIcons(position);
        }
        return this.cellIconsCache[position.sheetId][position.col][position.row];
    }
    getCellIconRect(icon, cellRect) {
        const cellPosition = icon.position;
        const cell = this.getters.getCell(cellPosition);
        const x = this.getIconHorizontalPosition(cellRect, icon.horizontalAlign, icon);
        const y = this.getters.computeTextYCoordinate(cellRect, icon.size, cell?.style?.verticalAlign);
        return { x: x, y: y, width: icon.size, height: icon.size };
    }
    getIconHorizontalPosition(rect, align, icon) {
        const start = rect.x;
        const end = rect.x + rect.width;
        switch (align) {
            case "right":
                return end - icon.margin - icon.size;
            case "left":
                return start + icon.margin;
            default:
                const centeringOffset = Math.floor((end - start - icon.size) / 2);
                return end - icon.size - centeringOffset;
        }
    }
    computeCellIcons(position) {
        const icons = { left: undefined, right: undefined, center: undefined };
        const callbacks = iconsOnCellRegistry.getAll();
        for (const callback of callbacks) {
            const icon = callback(this.getters, position);
            if (icon &&
                (!icons[icon.horizontalAlign] || icon.priority > icons[icon.horizontalAlign].priority)) {
                icons[icon.horizontalAlign] = icon;
            }
        }
        if (icons.center && (icons.left || icons.right)) {
            const sideIconsPriority = Math.max(icons.left?.priority || 0, icons.right?.priority || 0);
            if (icons.center.priority < sideIconsPriority) {
                icons.center = undefined;
            }
            else {
                icons.left = undefined;
                icons.right = undefined;
            }
        }
        return Object.values(icons).filter(isDefined);
    }
    doesCellHaveGridIcon(position) {
        return Boolean(this.getCellIcons(position).length);
    }
}

class DynamicTablesPlugin extends CoreViewPlugin {
    static getters = [
        "canCreateDynamicTableOnZones",
        "doesZonesContainFilter",
        "getFilter",
        "getFilters",
        "getTable",
        "getTables",
        "getTablesOverlappingZones",
        "getFilterId",
        "getFilterHeaders",
        "isFilterHeader",
    ];
    tables = {};
    handle(cmd) {
        if (invalidateEvaluationCommands.has(cmd.type) ||
            (cmd.type === "UPDATE_CELL" && ("content" in cmd || "format" in cmd)) ||
            cmd.type === "EVALUATE_CELLS") {
            this.tables = {};
            return;
        }
        switch (cmd.type) {
            case "CREATE_TABLE":
            case "REMOVE_TABLE":
            case "UPDATE_TABLE":
            case "DELETE_CONTENT":
                this.tables = {};
                break;
        }
    }
    finalize() {
        for (const sheetId of this.getters.getSheetIds()) {
            if (!this.tables[sheetId]) {
                this.tables[sheetId] = this.computeTables(sheetId);
            }
        }
    }
    computeTables(sheetId) {
        const tables = [];
        const coreTables = this.getters.getCoreTables(sheetId);
        // First we create the static tables, so we can use them to compute collision with dynamic tables
        for (const table of coreTables) {
            if (table.type === "dynamic")
                continue;
            tables.push(table);
        }
        const staticTables = [...tables];
        // Then we create the dynamic tables
        for (const coreTable of coreTables) {
            if (coreTable.type !== "dynamic")
                continue;
            const table = this.coreTableToTable(sheetId, coreTable);
            let tableZone = table.range.zone;
            // Reduce the zone to avoid collision with static tables. Per design, dynamic tables can't overlap with other
            // dynamic tables, because formulas cannot spread on the same area, so we don't need to check for that.
            for (const staticTable of staticTables) {
                const staticTableZone = staticTable.range.zone;
                if (!overlap(tableZone, staticTableZone)) {
                    continue;
                }
                if (staticTableZone.left > tableZone.left) {
                    tableZone = { ...tableZone, right: Math.min(tableZone.right, staticTableZone.left - 1) };
                }
                else {
                    tableZone = { ...tableZone, bottom: Math.min(tableZone.bottom, staticTableZone.top - 1) };
                }
            }
            tables.push({ ...table, range: this.getters.getRangeFromZone(sheetId, tableZone) });
        }
        return tables;
    }
    getFilters(sheetId) {
        return this.getTables(sheetId)
            .filter((table) => table.config.hasFilters)
            .map((table) => table.filters)
            .flat();
    }
    getTables(sheetId) {
        return this.tables[sheetId] || [];
    }
    getFilter(position) {
        const table = this.getTable(position);
        if (!table || !table.config.hasFilters) {
            return undefined;
        }
        return table.filters.find((filter) => filter.col === position.col);
    }
    getFilterId(position) {
        return this.getFilter(position)?.id;
    }
    getTable({ sheetId, col, row }) {
        return this.getTables(sheetId).find((table) => isInside(col, row, table.range.zone));
    }
    getTablesOverlappingZones(sheetId, zones) {
        return this.getTables(sheetId).filter((table) => zones.some((zone) => overlap(table.range.zone, zone)));
    }
    doesZonesContainFilter(sheetId, zones) {
        return this.getTablesOverlappingZones(sheetId, zones).some((table) => table.config.hasFilters);
    }
    getFilterHeaders(sheetId) {
        const headers = [];
        for (const table of this.getTables(sheetId)) {
            if (!table.config.hasFilters) {
                continue;
            }
            const zone = table.range.zone;
            const row = zone.top;
            for (let col = zone.left; col <= zone.right; col++) {
                headers.push({ sheetId, col, row });
            }
        }
        return headers;
    }
    isFilterHeader({ sheetId, col, row }) {
        const headers = this.getFilterHeaders(sheetId);
        return headers.some((header) => header.col === col && header.row === row);
    }
    /**
     * Check if we can create a dynamic table on the given zones.
     * - The zones must be continuous
     * - The union of the zones must be either:
     *    - A single cell that contains an array formula
     *    - All the spread cells of a single array formula
     */
    canCreateDynamicTableOnZones(sheetId, zones) {
        if (!areZonesContinuous(zones)) {
            return false;
        }
        const unionZone = union(...zones);
        const topLeft = { col: unionZone.left, row: unionZone.top, sheetId };
        const parentSpreadingCell = this.getters.getArrayFormulaSpreadingOn(topLeft);
        if (!parentSpreadingCell) {
            const evaluatedCell = this.getters.getEvaluatedCell(topLeft);
            return (evaluatedCell.value === CellErrorType.SpilledBlocked && !evaluatedCell.errorOriginPosition);
        }
        else if (deepEquals(parentSpreadingCell, topLeft) && getZoneArea(unionZone) === 1) {
            return true;
        }
        const zone = this.getters.getSpreadZone(parentSpreadingCell);
        return deepEquals(unionZone, zone);
    }
    coreTableToTable(sheetId, table) {
        if (table.type !== "dynamic") {
            return table;
        }
        const tableZone = table.range.zone;
        const tablePosition = { sheetId, col: tableZone.left, row: tableZone.top };
        const zone = this.getters.getSpreadZone(tablePosition) ?? table.range.zone;
        const range = this.getters.getRangeFromZone(sheetId, zone);
        const filters = this.getDynamicTableFilters(sheetId, table, zone);
        return { id: table.id, range, filters, config: table.config };
    }
    getDynamicTableFilters(sheetId, table, tableZone) {
        const filters = [];
        const { top, bottom, left, right } = tableZone;
        for (let col = left; col <= right; col++) {
            const tableColIndex = col - left;
            const zone = { left: col, right: col, top, bottom };
            const filter = createFilter(this.getDynamicTableFilterId(table.id, tableColIndex), this.getters.getRangeFromZone(sheetId, zone), table.config, this.getters.getRangeFromZone);
            filters.push(filter);
        }
        return filters;
    }
    getDynamicTableFilterId(tableId, tableCol) {
        return tableId + "_" + tableCol;
    }
    exportForExcel(data) {
        for (const sheet of data.sheets) {
            for (const tableData of sheet.tables) {
                const zone = toZone(tableData.range);
                const topLeft = { sheetId: sheet.id, col: zone.left, row: zone.top };
                const coreTable = this.getters.getCoreTable(topLeft);
                const table = this.getTable(topLeft);
                if (coreTable?.type !== "dynamic" || !table) {
                    continue;
                }
                tableData.range = zoneToXc(table.range.zone);
            }
        }
    }
}

class HeaderSizeUIPlugin extends CoreViewPlugin {
    static getters = ["getRowSize", "getHeaderSize", "getMaxAnchorOffset"];
    tallestCellInRow = {};
    ctx = document.createElement("canvas").getContext("2d");
    beforeHandle(cmd) {
        switch (cmd.type) {
            // Ensure rows are updated before "UPDATE_CELL" is dispatched from cell plugin.
            // "UPDATE_CELL" uses the Sheet core plugin to access row data.
            // If "ADD_COLUMNS_ROWS" has not been processed yet by header_sizes_ui,
            // size updates may apply to incorrect (pre-insert) rows.
            case "ADD_COLUMNS_ROWS":
                if (cmd.dimension === "COL") {
                    return;
                }
                const addIndex = getAddHeaderStartIndex(cmd.position, cmd.base);
                const newCells = Array(cmd.quantity).fill(undefined);
                const newTallestCells = insertItemsAtIndex(this.tallestCellInRow[cmd.sheetId], newCells, addIndex);
                this.history.update("tallestCellInRow", cmd.sheetId, newTallestCells);
                break;
        }
    }
    handle(cmd) {
        switch (cmd.type) {
            case "START":
            case "UPDATE_LOCALE":
                for (const sheetId of this.getters.getSheetIds()) {
                    this.initializeSheet(sheetId);
                }
                break;
            case "CREATE_SHEET": {
                this.initializeSheet(cmd.sheetId);
                break;
            }
            case "DUPLICATE_SHEET": {
                const tallestCells = deepCopy(this.tallestCellInRow[cmd.sheetId]);
                this.history.update("tallestCellInRow", cmd.sheetIdTo, tallestCells);
                break;
            }
            case "DELETE_SHEET":
                const tallestCells = { ...this.tallestCellInRow };
                delete tallestCells[cmd.sheetId];
                this.history.update("tallestCellInRow", tallestCells);
                break;
            case "REMOVE_COLUMNS_ROWS": {
                if (cmd.dimension === "COL") {
                    return;
                }
                const tallestCells = removeIndexesFromArray(this.tallestCellInRow[cmd.sheetId], cmd.elements);
                this.history.update("tallestCellInRow", cmd.sheetId, tallestCells);
                break;
            }
            case "RESIZE_COLUMNS_ROWS":
                {
                    const sheetId = cmd.sheetId;
                    if (cmd.dimension === "ROW") {
                        for (const row of cmd.elements) {
                            const tallestCell = this.getRowTallestCell(sheetId, row);
                            this.history.update("tallestCellInRow", sheetId, row, tallestCell);
                        }
                    }
                    else {
                        // Recompute row heights on col size change, they might have changed because of wrapped text
                        for (const row of range(0, this.getters.getNumberRows(sheetId))) {
                            for (const col of cmd.elements) {
                                this.updateRowSizeForCellChange(sheetId, row, col);
                            }
                        }
                    }
                }
                break;
            case "UPDATE_CELL":
                this.updateRowSizeForCellChange(cmd.sheetId, cmd.row, cmd.col);
                break;
            case "ADD_MERGE":
            case "REMOVE_MERGE":
                for (const target of cmd.target) {
                    for (const position of positions(target)) {
                        this.updateRowSizeForCellChange(cmd.sheetId, position.row, position.col);
                    }
                }
        }
        return;
    }
    getRowSize(sheetId, row) {
        return Math.round(this.getters.getUserRowSize(sheetId, row) ??
            this.tallestCellInRow[sheetId][row]?.size ??
            DEFAULT_CELL_HEIGHT);
    }
    getMaxAnchorOffset(sheetId, height, width) {
        let { numberOfRows: row, numberOfCols: col } = this.getters.getSheetSize(sheetId);
        let availableHeight = 0;
        for (; availableHeight < height && row > 0; row--) {
            availableHeight += this.getRowSize(sheetId, row - 1);
        }
        let availableWidth = 0;
        for (; availableWidth < width && col > 0; col--) {
            availableWidth += this.getters.getColSize(sheetId, col - 1);
        }
        return {
            col,
            row,
            offset: { x: availableWidth - width, y: availableHeight - height },
        };
    }
    getHeaderSize(sheetId, dimension, index) {
        if (this.getters.isHeaderHidden(sheetId, dimension, index)) {
            return 0;
        }
        return dimension === "ROW"
            ? this.getRowSize(sheetId, index)
            : this.getters.getColSize(sheetId, index);
    }
    updateRowSizeForCellChange(sheetId, row, col) {
        const tallestCellInRow = this.tallestCellInRow[sheetId]?.[row];
        if (tallestCellInRow?.cell.col === col) {
            const newTallestCell = this.getRowTallestCell(sheetId, row);
            this.history.update("tallestCellInRow", sheetId, row, newTallestCell);
        }
        const updatedCellHeight = this.getCellHeight({ sheetId, col, row });
        if (updatedCellHeight <= DEFAULT_CELL_HEIGHT) {
            return;
        }
        if ((!tallestCellInRow && updatedCellHeight > DEFAULT_CELL_HEIGHT) ||
            (tallestCellInRow && updatedCellHeight > tallestCellInRow.size)) {
            const newTallestCell = { cell: { sheetId, col, row }, size: updatedCellHeight };
            this.history.update("tallestCellInRow", sheetId, row, newTallestCell);
        }
    }
    initializeSheet(sheetId) {
        const tallestCells = [];
        for (let row = 0; row < this.getters.getNumberRows(sheetId); row++) {
            const tallestCell = this.getRowTallestCell(sheetId, row);
            tallestCells.push(tallestCell);
        }
        this.history.update("tallestCellInRow", sheetId, tallestCells);
    }
    /**
     * Return the height the cell should have in the sheet, which is either DEFAULT_CELL_HEIGHT if the cell is in a multi-row
     * merge, or the height of the cell computed based on its style/content.
     */
    getCellHeight(position) {
        if (this.isInMultiRowMerge(position)) {
            return DEFAULT_CELL_HEIGHT;
        }
        const cell = this.getters.getCell(position);
        const colSize = this.getters.getColSize(position.sheetId, position.col);
        return getDefaultCellHeight(this.ctx, cell, this.getters.getLocale(), colSize);
    }
    isInMultiRowMerge(position) {
        const merge = this.getters.getMerge(position);
        return !!merge && merge.bottom !== merge.top;
    }
    /**
     * Get the tallest cell of a row and its size.
     */
    getRowTallestCell(sheetId, row) {
        const userRowSize = this.getters.getUserRowSize(sheetId, row);
        if (userRowSize !== undefined) {
            return undefined;
        }
        const cellIds = this.getters.getRowCells(sheetId, row);
        let maxHeight = 0;
        let tallestCell = undefined;
        for (let i = 0; i < cellIds.length; i++) {
            const cell = this.getters.getCellById(cellIds[i]);
            if (!cell) {
                continue;
            }
            const position = this.getters.getCellPosition(cell.id);
            const cellHeight = this.getCellHeight(position);
            if (cellHeight > maxHeight && cellHeight > DEFAULT_CELL_HEIGHT) {
                maxHeight = cellHeight;
                tallestCell = { cell: position, size: cellHeight };
            }
        }
        if (tallestCell && tallestCell.size > DEFAULT_CELL_HEIGHT) {
            return tallestCell;
        }
        return undefined;
    }
}

const PERCENT_FORMAT = "0.00%";
/**
 * Dynamically creates a presentation layer wrapper around a given pivot class.
 *
 * It allows to implement additional behaviors and features that can be applied
 * to all pivots, regardless of the specific pivot implementation.
 * Examples of such features include calculated measures or "Show value as" options.
 */
function withPivotPresentationLayer (PivotClass) {
    class PivotPresentationLayer extends PivotClass {
        getters;
        pivotId;
        cache = {};
        rankAsc = {};
        rankDesc = {};
        runningTotal = {};
        runningTotalInPercent = {};
        constructor(pivotId, custom, params) {
            super(custom, params);
            this.getters = params.getters;
            this.pivotId = pivotId;
        }
        markAsDirtyForEvaluation() {
            this.cache = {};
            this.rankAsc = {};
            this.rankDesc = {};
            this.runningTotal = {};
            this.runningTotalInPercent = {};
            super.markAsDirtyForEvaluation?.();
        }
        getPivotCellValueAndFormat(measureName, domain) {
            return this.getMeasureDisplayValue(measureName, domain);
        }
        _getPivotCellValueAndFormat(measureName, domain) {
            const cacheKey = `${measureName}-${domain
                .map((node) => node.field + "=" + node.value)
                .join(",")}`;
            if (this.cache[cacheKey]) {
                return this.cache[cacheKey];
            }
            const measure = this.getMeasure(measureName);
            const result = measure.computedBy
                ? this.computeMeasure(measure, domain)
                : super.getPivotCellValueAndFormat(measureName, domain);
            if (measure.format) {
                this.cache[cacheKey] = { ...result, format: measure.format };
            }
            else {
                this.cache[cacheKey] = result;
            }
            return this.cache[cacheKey];
        }
        computeMeasure(measure, domain) {
            if (!measure.computedBy) {
                return { value: 0 };
            }
            const { columns, rows } = super.definition;
            if (measure.aggregator && columns.length + rows.length !== domain.length) {
                const values = this.getValuesToAggregate(measure, domain);
                const aggregator = AGGREGATORS_FN[measure.aggregator];
                if (!aggregator) {
                    return { value: 0 };
                }
                try {
                    return aggregator([values], this.getters.getLocale());
                }
                catch (error) {
                    return handleError(error, measure.aggregator.toUpperCase());
                }
            }
            const formula = this.getters.getMeasureCompiledFormula(this.pivotId, measure);
            const getSymbolValue = (symbolName) => {
                const { columns, rows } = this.definition;
                if (columns.find((col) => col.nameWithGranularity === symbolName)) {
                    const { colDomain } = domainToColRowDomain(this, domain);
                    const symbolIndex = colDomain.findIndex((node) => node.field === symbolName);
                    if (symbolIndex === -1) {
                        return new NotAvailableError();
                    }
                    return this.getPivotHeaderValueAndFormat(colDomain.slice(0, symbolIndex + 1));
                }
                if (rows.find((row) => row.nameWithGranularity === symbolName)) {
                    const { rowDomain } = domainToColRowDomain(this, domain);
                    const symbolIndex = rowDomain.findIndex((row) => row.field === symbolName);
                    if (symbolIndex === -1) {
                        return new NotAvailableError();
                    }
                    return this.getPivotHeaderValueAndFormat(rowDomain.slice(0, symbolIndex + 1));
                }
                return this.getPivotCellValueAndFormat(symbolName, domain);
            };
            const result = this.getters.evaluateCompiledFormula(measure.computedBy.sheetId, formula, getSymbolValue);
            if (isMatrix(result)) {
                return result[0][0];
            }
            return result;
        }
        getValuesToAggregate(measure, domain) {
            const { rowDomain, colDomain } = domainToColRowDomain(this, domain);
            const table = super.getExpandedTableStructure();
            const values = [];
            if (colDomain.length === 0 &&
                rowDomain.length < this.definition.rows.length &&
                this.definition.rows.length &&
                this.definition.columns.length) {
                const colDomains = this.treeToLeafDomains(table.getColTree());
                const rowSubTree = this.getSubTreeMatchingDomain(table.getRowTree(), rowDomain);
                const rowDomains = this.treeToLeafDomains(rowSubTree);
                for (const colDomain of colDomains) {
                    for (const subRowDomain of rowDomains) {
                        values.push(this._getPivotCellValueAndFormat(measure.id, rowDomain.concat(subRowDomain).concat(colDomain)));
                    }
                }
                return values;
            }
            else if (rowDomain.length === this.definition.rows.length && colDomain.length === 0) {
                // aggregate a row in the last column
                const tree = table.getColTree();
                const subTree = this.getSubTreeMatchingDomain(tree, colDomain);
                const domains = this.treeToLeafDomains(subTree, colDomain);
                for (const domain of domains) {
                    values.push(this._getPivotCellValueAndFormat(measure.id, rowDomain.concat(domain)));
                }
                return values;
            }
            else if (rowDomain.length === this.definition.rows.length &&
                colDomain.length &&
                colDomain.length < this.definition.columns.length) {
                const colSubTree = this.getSubTreeMatchingDomain(table.getColTree(), colDomain);
                const domains = this.treeToLeafDomains(colSubTree, colDomain);
                for (const domain of domains) {
                    values.push(this._getPivotCellValueAndFormat(measure.id, rowDomain.concat(domain)));
                }
                return values;
            }
            else {
                const tree = table.getRowTree();
                const subTree = this.getSubTreeMatchingDomain(tree, rowDomain);
                const domains = this.treeToLeafDomains(subTree, rowDomain);
                for (const domain of domains) {
                    values.push(this._getPivotCellValueAndFormat(measure.id, domain.concat(colDomain)));
                }
                return values;
            }
        }
        getSubTreeMatchingDomain(tree, domain, domainLevel = 0) {
            if (domainLevel > domain.length) {
                return [];
            }
            if (domain.length === domainLevel) {
                return tree;
            }
            for (const node of tree) {
                const dimension = this.definition.getDimension(node.field);
                const normalizedValue = toNormalizedPivotValue(dimension, domain[domainLevel]?.value);
                if (node.field === domain[domainLevel]?.field && node.value === normalizedValue) {
                    return this.getSubTreeMatchingDomain(node.children, domain, domainLevel + 1);
                }
            }
            return [];
        }
        treeToLeafDomains(tree, parentDomain = []) {
            const domains = [];
            for (const node of tree) {
                const dimension = this.definition.getDimension(node.field);
                const nodeDomain = [
                    ...parentDomain,
                    { field: node.field, value: node.value, type: dimension.type },
                ];
                if (node.children.length === 0) {
                    domains.push(nodeDomain);
                }
                else {
                    domains.push(...this.treeToLeafDomains(node.children, nodeDomain));
                }
            }
            return domains;
        }
        getMeasureDisplayValue(measureId, domain) {
            const measure = this.getMeasure(measureId);
            const rawValue = this._getPivotCellValueAndFormat(measureId, domain);
            if (!measure.display || measure.display.type === "no_calculations" || rawValue.message) {
                return rawValue;
            }
            const fieldName = measure.display.fieldNameWithGranularity;
            if (fieldName && !this.isFieldInPivot(fieldName)) {
                return {
                    value: CellErrorType.NotAvailable,
                    message: _t('Field "%s" not found in pivot for measure display calculation', fieldName),
                };
            }
            try {
                const display = measure.display;
                switch (display.type) {
                    case "%_of_grand_total":
                        return this.asPercentOfGrandTotal(rawValue, measure);
                    case "%_of_col_total":
                        return this.asPercentOfColumnTotal(rawValue, measure, domain);
                    case "%_of_row_total":
                        return this.asPercentOfRowTotal(rawValue, measure, domain);
                    case "%_of_parent_row_total":
                        return this.asPercentOfParentRowTotal(rawValue, measure, domain);
                    case "%_of_parent_col_total":
                        return this.asPercentOfParentColumnTotal(rawValue, measure, domain);
                    case "index":
                        return this.asIndex(rawValue, measure, domain);
                    case "%_of_parent_total":
                        return this.asPercentOfParentTotal(rawValue, measure, domain, display);
                    case "running_total":
                        return this.asRunningTotal(rawValue, measure, domain, display, "running_total");
                    case "%_running_total":
                        return this.asRunningTotal(rawValue, measure, domain, display, "%_running_total");
                    case "rank_asc":
                        return this.asRank(rawValue, measure, domain, display, "asc");
                    case "rank_desc":
                        return this.asRank(rawValue, measure, domain, display, "desc");
                    case "%_of":
                        return this.asPercentOf(rawValue, measure, domain, display);
                    case "difference_from":
                        return this.asDifferenceFrom(rawValue, measure, domain, display);
                    case "%_difference_from":
                        return this.asDifferenceFromInPercent(rawValue, measure, domain, display);
                }
                return rawValue;
            }
            catch (e) {
                return handleError(e, "COMPUTE_MEASURE_DISPLAY_VALUE");
            }
        }
        asPercentOfGrandTotal(rawValue, measure) {
            const grandTotal = this.getGrandTotal(measure.id);
            return grandTotal === 0
                ? { value: CellErrorType.DivisionByZero }
                : { value: this.measureValueToNumber(rawValue) / grandTotal, format: PERCENT_FORMAT };
        }
        asPercentOfRowTotal(rawValue, measure, domain) {
            const rowTotal = this.getRowTotal(measure.id, domain);
            return rowTotal === 0
                ? { value: CellErrorType.DivisionByZero }
                : { value: this.measureValueToNumber(rawValue) / rowTotal, format: PERCENT_FORMAT };
        }
        asPercentOfColumnTotal(rawValue, measure, domain) {
            const columnTotal = this.getColumnTotal(measure.id, domain);
            return columnTotal === 0
                ? { value: CellErrorType.DivisionByZero }
                : { value: this.measureValueToNumber(rawValue) / columnTotal, format: PERCENT_FORMAT };
        }
        asPercentOfParentRowTotal(rawValue, measure, domain) {
            const parentRowDomain = getDomainOfParentRow(this, domain);
            const parentRowValue = this.measureValueToNumber(this._getPivotCellValueAndFormat(measure.id, parentRowDomain));
            return parentRowValue === 0
                ? { value: "" }
                : { value: this.measureValueToNumber(rawValue) / parentRowValue, format: PERCENT_FORMAT };
        }
        asPercentOfParentColumnTotal(rawValue, measure, domain) {
            const parentColumnDomain = getDomainOfParentCol(this, domain);
            const parentColValue = this.measureValueToNumber(this._getPivotCellValueAndFormat(measure.id, parentColumnDomain));
            return parentColValue === 0
                ? { value: "" }
                : { value: this.measureValueToNumber(rawValue) / parentColValue, format: PERCENT_FORMAT };
        }
        asPercentOfParentTotal(rawValue, measure, domain, display) {
            const { fieldNameWithGranularity } = display;
            if (!fieldNameWithGranularity) {
                return rawValue;
            }
            if (!isFieldInDomain(fieldNameWithGranularity, domain)) {
                return { value: "" };
            }
            const parentDomain = getFieldParentDomain(this, fieldNameWithGranularity, domain);
            const parentTotal = this._getPivotCellValueAndFormat(measure.id, parentDomain);
            const parentTotalValue = this.measureValueToNumber(parentTotal);
            return parentTotalValue === 0
                ? { value: "" }
                : { value: this.measureValueToNumber(rawValue) / parentTotalValue, format: PERCENT_FORMAT };
        }
        asIndex(rawValue, measure, domain) {
            const value = this.measureValueToNumber(rawValue);
            const parentRowTotal = this.getRowTotal(measure.id, domain);
            const parentColTotal = this.getColumnTotal(measure.id, domain);
            const grandTotal = this.getGrandTotal(measure.id);
            return parentRowTotal === 0 || parentColTotal === 0
                ? { value: CellErrorType.DivisionByZero }
                : { value: (value * grandTotal) / (parentColTotal * parentRowTotal), format: undefined };
        }
        asRunningTotal(rawValue, measure, domain, display, mode) {
            const { fieldNameWithGranularity } = display;
            if (!fieldNameWithGranularity) {
                return rawValue;
            }
            const totalCache = mode === "running_total" ? this.runningTotal : this.runningTotalInPercent;
            let runningTotals = totalCache[measure.id]?.[fieldNameWithGranularity];
            if (!runningTotals) {
                runningTotals = this.computeRunningTotal(measure, fieldNameWithGranularity, mode);
                if (!totalCache[measure.id]) {
                    totalCache[measure.id] = {};
                }
                totalCache[measure.id][fieldNameWithGranularity] = runningTotals;
            }
            const { rowDomain, colDomain } = domainToColRowDomain(this, domain);
            const colDomainKey = domainToString(colDomain);
            const rowDomainKey = domainToString(rowDomain);
            const runningTotal = runningTotals[colDomainKey]?.[rowDomainKey];
            return {
                value: runningTotal ?? "",
                format: mode === "running_total" ? rawValue.format : PERCENT_FORMAT,
            };
        }
        asPercentOf(rawValue, measure, domain, display) {
            const { fieldNameWithGranularity, value } = display;
            if (value === undefined || !fieldNameWithGranularity) {
                return rawValue;
            }
            if (!isFieldInDomain(fieldNameWithGranularity, domain)) {
                return { value: "" };
            }
            const comparedValue = this.getComparisonValue(measure, domain, fieldNameWithGranularity, value);
            const numberValue = this.strictMeasureValueToNumber(rawValue);
            if (comparedValue === 0 || (comparedValue === "sameValue" && numberValue === 0)) {
                return { value: CellErrorType.DivisionByZero };
            }
            else if (!comparedValue || (comparedValue === "sameValue" && !numberValue)) {
                return { value: "" };
            }
            else if (comparedValue === "sameValue") {
                return { value: 1, format: PERCENT_FORMAT };
            }
            else if (numberValue === undefined) {
                return { value: CellErrorType.NullError };
            }
            return { value: numberValue / comparedValue, format: PERCENT_FORMAT };
        }
        asDifferenceFrom(rawValue, measure, domain, display) {
            const { fieldNameWithGranularity, value } = display;
            if (value === undefined || !fieldNameWithGranularity) {
                return rawValue;
            }
            if (!isFieldInDomain(fieldNameWithGranularity, domain)) {
                return { value: "" };
            }
            const comparedValue = this.getComparisonValue(measure, domain, fieldNameWithGranularity, value) || 0;
            return comparedValue === "sameValue"
                ? { value: "" }
                : {
                    value: this.measureValueToNumber(rawValue) - comparedValue,
                    format: rawValue.format,
                };
        }
        asDifferenceFromInPercent(rawValue, measure, domain, display) {
            const { fieldNameWithGranularity, value } = display;
            if (value === undefined || !fieldNameWithGranularity) {
                return rawValue;
            }
            if (!isFieldInDomain(fieldNameWithGranularity, domain)) {
                return { value: "" };
            }
            const comparedValue = this.getComparisonValue(measure, domain, fieldNameWithGranularity, value);
            const numberValue = this.strictMeasureValueToNumber(rawValue);
            if (comparedValue === 0) {
                return { value: CellErrorType.DivisionByZero };
            }
            else if (!comparedValue || comparedValue === "sameValue") {
                return { value: "" };
            }
            else if (numberValue === undefined) {
                return { value: CellErrorType.NullError };
            }
            return { value: (numberValue - comparedValue) / comparedValue, format: PERCENT_FORMAT };
        }
        asRank(rawValue, measure, domain, display, order) {
            const { fieldNameWithGranularity } = display;
            if (!fieldNameWithGranularity) {
                return rawValue;
            }
            if (!isFieldInDomain(fieldNameWithGranularity, domain)) {
                return { value: "" };
            }
            const rankingCache = order === "asc" ? this.rankAsc : this.rankDesc;
            let ranking = rankingCache[measure.id]?.[fieldNameWithGranularity];
            if (!ranking) {
                ranking = this.computeRank(measure, fieldNameWithGranularity, order);
                if (!rankingCache[measure.id]) {
                    rankingCache[measure.id] = {};
                }
                rankingCache[measure.id][fieldNameWithGranularity] = ranking;
            }
            const { rowDomain, colDomain } = domainToColRowDomain(this, domain);
            const colDomainKey = domainToString(colDomain);
            const rowDomainKey = domainToString(rowDomain);
            const rank = ranking[colDomainKey]?.[rowDomainKey];
            return { value: rank ?? "" };
        }
        computeRank(measure, fieldNameWithGranularity, order) {
            const ranking = {};
            const mainDimension = getFieldDimensionType(this, fieldNameWithGranularity);
            const secondaryDimension = mainDimension === "row" ? "column" : "row";
            let pivotCells = this.getPivotValueCells(measure.id);
            if (mainDimension === "column") {
                // Transpose the pivot cells so we can do the same operations on the columns as on the rows
                // This means that we need to transpose back the ranking at the end
                pivotCells = transposeMatrix(pivotCells);
            }
            for (const col of pivotCells) {
                const colDomain = getDimensionDomain(this, secondaryDimension, col[0].domain);
                const colDomainKey = domainToString(colDomain);
                const cells = col
                    .map((cell) => ({
                    ...cell,
                    value: this.strictMeasureValueToNumber(this._getPivotCellValueAndFormat(measure.id, cell.domain)),
                    rowDomain: getDimensionDomain(this, mainDimension, cell.domain),
                }))
                    .filter((cell) => isFieldInDomain(fieldNameWithGranularity, cell.rowDomain));
                // Group the cells by ranking domain, and sort them to get the ranking
                const groupedCell = Object.groupBy(cells, (cell) => getRankingDomainKey(cell.rowDomain, fieldNameWithGranularity));
                for (const rankingDomainKey in groupedCell) {
                    groupedCell[rankingDomainKey] = removeDuplicates$1(groupedCell[rankingDomainKey] || [], (cell) => cell.value)
                        .filter((cell) => cell.value !== undefined)
                        .sort((a, b) => (order === "asc" ? a.value - b.value : b.value - a.value));
                }
                ranking[colDomainKey] = {};
                for (const cell of cells) {
                    const rowDomain = getDimensionDomain(this, mainDimension, cell.domain);
                    const rowDomainKey = domainToString(rowDomain);
                    const rankingDomainKey = getRankingDomainKey(cell.rowDomain, fieldNameWithGranularity);
                    const group = groupedCell[rankingDomainKey];
                    if (!group) {
                        continue;
                    }
                    const rank = group.findIndex((c) => c.value === cell.value);
                    if (rank !== -1) {
                        ranking[colDomainKey][rowDomainKey] = rank + 1; // Ranks start at 1
                    }
                }
            }
            return mainDimension === "row" ? ranking : transpose2dPOJO(ranking);
        }
        computeRunningTotal(measure, fieldNameWithGranularity, mode) {
            const cellsRunningTotals = {};
            const mainDimension = getFieldDimensionType(this, fieldNameWithGranularity);
            const secondaryDimension = mainDimension === "row" ? "column" : "row";
            let pivotCells = this.getPivotValueCells(measure.id);
            if (mainDimension === "column") {
                // Transpose the pivot cells so we can do the same operations on the columns as on the rows
                // This means that we need to transpose back the totals at the end
                pivotCells = transposeMatrix(pivotCells);
            }
            for (const col of pivotCells) {
                const colDomain = getDimensionDomain(this, secondaryDimension, col[0].domain);
                const colDomainKey = domainToString(colDomain);
                cellsRunningTotals[colDomainKey] = {};
                const runningTotals = {};
                const cellsWithValue = col
                    .map((cell) => ({
                    ...cell,
                    rowDomain: getDimensionDomain(this, mainDimension, cell.domain),
                    value: this.measureValueToNumber(this._getPivotCellValueAndFormat(measure.id, cell.domain)),
                }))
                    .filter((cell) => isFieldInDomain(fieldNameWithGranularity, cell.rowDomain));
                for (const cell of cellsWithValue) {
                    const rowDomainKey = domainToString(cell.rowDomain);
                    const runningTotalKey = getRunningTotalDomainKey(cell.rowDomain, fieldNameWithGranularity);
                    const runningTotal = (runningTotals[runningTotalKey] || 0) + cell.value;
                    runningTotals[runningTotalKey] = runningTotal;
                    cellsRunningTotals[colDomainKey][rowDomainKey] = runningTotal;
                }
                if (mode === "%_running_total") {
                    for (const cell of cellsWithValue) {
                        const rowDomain = cell.rowDomain;
                        const rowDomainKey = domainToString(rowDomain);
                        const runningTotalKey = getRunningTotalDomainKey(rowDomain, fieldNameWithGranularity);
                        const cellRunningTotal = cellsRunningTotals[colDomainKey][rowDomainKey] || 0;
                        const finalRunningTotal = runningTotals[runningTotalKey];
                        cellsRunningTotals[colDomainKey][rowDomainKey] = !finalRunningTotal
                            ? undefined
                            : cellRunningTotal / finalRunningTotal;
                    }
                }
            }
            return mainDimension === "row" ? cellsRunningTotals : transpose2dPOJO(cellsRunningTotals);
        }
        getGrandTotal(measureId) {
            const grandTotal = this._getPivotCellValueAndFormat(measureId, []);
            return this.measureValueToNumber(grandTotal);
        }
        getRowTotal(measureId, domain) {
            const totalDomain = domainToColRowDomain(this, domain).rowDomain;
            const rowTotal = this._getPivotCellValueAndFormat(measureId, totalDomain);
            return this.measureValueToNumber(rowTotal);
        }
        getColumnTotal(measureId, domain) {
            const totalDomain = domainToColRowDomain(this, domain).colDomain;
            const columnTotal = this._getPivotCellValueAndFormat(measureId, totalDomain);
            return this.measureValueToNumber(columnTotal);
        }
        isFieldInPivot(nameWithGranularity) {
            return (this.definition.columns.some((c) => c.nameWithGranularity === nameWithGranularity) ||
                this.definition.rows.some((r) => r.nameWithGranularity === nameWithGranularity));
        }
        /**
         * With the given measure, fetch the value of the cell in the pivot that has the given domain with
         * the value of the field `fieldNameWithGranularity` replaced by `valueToCompare`.
         *
         * @param valueToCompare either a value to replace the field value with, or "(previous)" or "(next)"
         * @returns the value of the cell in the pivot with the new domain, or "sameValue" if the domain is the same
         */
        getComparisonValue(measure, domain, fieldNameWithGranularity, valueToCompare) {
            const comparedDomain = valueToCompare === PREVIOUS_VALUE || valueToCompare === NEXT_VALUE
                ? getPreviousOrNextValueDomain(this, domain, fieldNameWithGranularity, valueToCompare)
                : replaceFieldValueInDomain(domain, fieldNameWithGranularity, valueToCompare);
            if (deepEquals(comparedDomain, domain)) {
                return "sameValue";
            }
            if (!comparedDomain || !isDomainIsInPivot(this, comparedDomain)) {
                throw new NotAvailableError();
            }
            const comparedValue = this._getPivotCellValueAndFormat(measure.id, comparedDomain);
            return this.strictMeasureValueToNumber(comparedValue);
        }
        getPivotValueCells(measureId) {
            return this.getCollapsedTableStructure()
                .getPivotCells()
                .map((col) => col.filter((cell) => cell.type === "VALUE" && cell.measure === measureId))
                .filter((col) => col.length > 0);
        }
        measureValueToNumber(result) {
            if (typeof result.value === "number") {
                return result.value;
            }
            if (!result.value) {
                return 0;
            }
            // Should not happen, measures aggregates are always numbers or undefined
            throw new Error(`Value ${result.value} is not a number`);
        }
        strictMeasureValueToNumber(result) {
            if (typeof result.value === "number") {
                return result.value;
            }
            if (!result.value) {
                return undefined;
            }
            throw new Error(`Value ${result.value} is not a number`);
        }
        getCollapsedTableStructure() {
            const table = super.getCollapsedTableStructure();
            this.sortTableStructure(table);
            return table;
        }
        getExpandedTableStructure() {
            const table = super.getExpandedTableStructure();
            this.sortTableStructure(table);
            return table;
        }
        sortTableStructure(table) {
            if (!this.definition.sortedColumn || table.isSorted) {
                return;
            }
            const measure = this.definition.sortedColumn.measure;
            const isSortValid = isSortedColumnValid(this.definition.sortedColumn, this);
            if (isSortValid) {
                table.sort(measure, this.definition.sortedColumn, (measure, domain) => this._getPivotCellValueAndFormat(measure, domain));
            }
        }
    }
    return PivotPresentationLayer;
}

const UNDO_REDO_PIVOT_COMMANDS = ["ADD_PIVOT", "UPDATE_PIVOT", "REMOVE_PIVOT"];
function isPivotCommand(cmd) {
    return UNDO_REDO_PIVOT_COMMANDS.includes(cmd.type);
}
class PivotUIPlugin extends CoreViewPlugin {
    static getters = [
        "getPivot",
        "getFirstPivotFunction",
        "getPivotCellSortDirection",
        "getPivotIdFromPosition",
        "getPivotCellFromPosition",
        "generateNewCalculatedMeasureName",
        "isPivotUnused",
        "isSpillPivotFormula",
    ];
    pivots = {};
    unusedPivots;
    custom;
    constructor(config) {
        super(config);
        this.custom = config.custom;
    }
    beforeHandle(cmd) {
        switch (cmd.type) {
            case "START":
                for (const pivotId of this.getters.getPivotIds()) {
                    this.setupPivot(pivotId);
                }
        }
    }
    handle(cmd) {
        if (invalidateEvaluationCommands.has(cmd.type)) {
            for (const pivotId of this.getters.getPivotIds()) {
                this.setupPivot(pivotId, { recreate: true });
            }
        }
        switch (cmd.type) {
            case "REFRESH_PIVOT":
                this.refreshPivot(cmd.id);
                break;
            case "ADD_PIVOT": {
                this.setupPivot(cmd.pivotId);
                break;
            }
            case "DUPLICATE_PIVOT": {
                this.setupPivot(cmd.newPivotId);
                break;
            }
            case "UPDATE_PIVOT": {
                this.setupPivot(cmd.pivotId, { recreate: true });
                break;
            }
            case "DELETE_SHEET":
            case "UPDATE_CELL": {
                this.unusedPivots = undefined;
                break;
            }
            case "UNDO":
            case "REDO": {
                this.unusedPivots = undefined;
                const pivotCommands = cmd.commands.filter(isPivotCommand);
                for (const cmd of pivotCommands) {
                    const pivotId = cmd.pivotId;
                    if (!this.getters.isExistingPivot(pivotId)) {
                        continue;
                    }
                    this.setupPivot(pivotId, { recreate: true });
                }
                break;
            }
            case "UPDATE_LOCALE":
                /**
                 * Reset the cache of the date/datetime pivot values, as it depends on
                 * the locale. (e.g. the first day of the week)
                 */
                resetMapValueDimensionDate();
                break;
        }
    }
    // ---------------------------------------------------------------------
    // Getters
    // ---------------------------------------------------------------------
    /**
     * Get the id of the pivot at the given position. Returns undefined if there
     * is no pivot at this position
     */
    getPivotIdFromPosition(position) {
        const cell = this.getters.getCorrespondingFormulaCell(position);
        if (cell && cell.isFormula) {
            const pivotFunction = this.getFirstPivotFunction(position.sheetId, cell.compiledFormula.tokens);
            if (pivotFunction) {
                const pivotId = pivotFunction.args[0]?.toString();
                return pivotId && this.getters.getPivotId(pivotId);
            }
        }
        return undefined;
    }
    isSpillPivotFormula(position) {
        const cell = this.getters.getCorrespondingFormulaCell(position);
        if (cell && cell.isFormula) {
            const pivotFunction = this.getFirstPivotFunction(position.sheetId, cell.compiledFormula.tokens);
            return pivotFunction?.functionName === "PIVOT";
        }
        return false;
    }
    getFirstPivotFunction(sheetId, tokens) {
        const pivotFunction = getFirstPivotFunction(tokens);
        if (!pivotFunction) {
            return undefined;
        }
        const { functionName, args } = pivotFunction;
        const evaluatedArgs = args.map((argAst) => {
            if (argAst.type === "EMPTY") {
                return undefined;
            }
            else if (argAst.type === "STRING" ||
                argAst.type === "BOOLEAN" ||
                argAst.type === "NUMBER") {
                return argAst.value;
            }
            const argsString = astToFormula(argAst);
            return this.getters.evaluateFormula(sheetId, argsString);
        });
        return { functionName, args: evaluatedArgs };
    }
    /**
     * Returns the domain args of a pivot formula from a position.
     * For all those formulas:
     *
     * =PIVOT.VALUE(1,"expected_revenue","stage_id",2,"city","Brussels")
     * =PIVOT.HEADER(1,"stage_id",2,"city","Brussels")
     * =PIVOT.HEADER(1,"stage_id",2,"city","Brussels","measure","expected_revenue")
     *
     * the result is the same: ["stage_id", 2, "city", "Brussels"]
     *
     * If the cell is the result of PIVOT, the result is the domain of the cell
     * as if it was the individual pivot formula
     */
    getPivotCellFromPosition(position) {
        const cell = this.getters.getCorrespondingFormulaCell(position);
        if (!cell || !cell.isFormula || getNumberOfPivotFunctions(cell.compiledFormula.tokens) === 0) {
            return EMPTY_PIVOT_CELL;
        }
        const mainPosition = this.getters.getCellPosition(cell.id);
        const result = this.getters.getFirstPivotFunction(position.sheetId, cell.compiledFormula.tokens);
        if (!result) {
            return EMPTY_PIVOT_CELL;
        }
        let { functionName, args } = result;
        const formulaId = args[0];
        if (!formulaId) {
            return EMPTY_PIVOT_CELL;
        }
        const pivotId = this.getters.getPivotId(formulaId.toString());
        if (!pivotId) {
            return EMPTY_PIVOT_CELL;
        }
        const pivot = this.getPivot(pivotId);
        if (!pivot.isValid()) {
            return EMPTY_PIVOT_CELL;
        }
        if (functionName === "PIVOT" &&
            !cell.content.replaceAll(" ", "").toUpperCase().startsWith("=PIVOT")) {
            return EMPTY_PIVOT_CELL;
        }
        if (functionName === "PIVOT") {
            const includeTotal = toScalar(args[2]);
            const shouldIncludeTotal = includeTotal === undefined ? true : toBoolean(includeTotal);
            const includeColumnHeaders = toScalar(args[3]);
            const includeMeasures = toScalar(args[5]);
            const shouldIncludeMeasures = includeMeasures === undefined ? true : toBoolean(includeMeasures);
            const shouldIncludeColumnHeaders = includeColumnHeaders === undefined ? true : toBoolean(includeColumnHeaders);
            const visibilityOptions = {
                displayColumnHeaders: shouldIncludeColumnHeaders,
                displayTotals: shouldIncludeTotal,
                displayMeasuresRow: shouldIncludeMeasures,
            };
            const pivotCells = pivot.getCollapsedTableStructure().getPivotCells(visibilityOptions);
            const pivotCol = position.col - mainPosition.col;
            const pivotRow = position.row - mainPosition.row;
            return pivotCells[pivotCol][pivotRow];
        }
        try {
            const offsetRow = position.row - mainPosition.row;
            const offsetCol = position.col - mainPosition.col;
            args = args.map((arg) => (isMatrix(arg) ? arg[offsetCol][offsetRow] : arg));
            if (functionName === "PIVOT.HEADER" && args.at(-2) === "measure") {
                const domain = pivot.parseArgsToPivotDomain(args.slice(1, -2).map((value) => ({ value })));
                return {
                    type: "MEASURE_HEADER",
                    domain,
                    measure: args.at(-1)?.toString() || "",
                };
            }
            else if (functionName === "PIVOT.HEADER") {
                const domain = pivot.parseArgsToPivotDomain(args.slice(1).map((value) => ({ value })));
                const colRowDomain = domainToColRowDomain(pivot, domain);
                return {
                    type: "HEADER",
                    domain,
                    dimension: colRowDomain.colDomain.length ? "COL" : "ROW",
                };
            }
            const [measure, ...domainArgs] = args.slice(1);
            const domain = pivot.parseArgsToPivotDomain(domainArgs.map((value) => ({ value })));
            return {
                type: "VALUE",
                domain,
                measure: measure?.toString() || "",
            };
        }
        catch (_) {
            return EMPTY_PIVOT_CELL;
        }
    }
    generateNewCalculatedMeasureName(measures) {
        const existingMeasures = measures.map((m) => m.fieldName);
        return getUniqueText(_t("Calculated measure 1"), existingMeasures, {
            compute: (name, i) => _t("Calculated measure %s", i),
        });
    }
    getPivot(pivotId) {
        if (!this.getters.isExistingPivot(pivotId)) {
            throw new Error(`pivot ${pivotId} not found`);
        }
        return this.pivots[pivotId];
    }
    isPivotUnused(pivotId) {
        return this._getUnusedPivots().includes(pivotId);
    }
    getPivotCellSortDirection(position) {
        const pivotId = this.getters.getPivotIdFromPosition(position);
        const pivotCell = this.getters.getPivotCellFromPosition(position);
        if (pivotCell.type === "EMPTY" || pivotCell.type === "HEADER" || !pivotId) {
            return undefined;
        }
        const pivot = this.getters.getPivot(pivotId);
        const colDomain = domainToColRowDomain(pivot, pivotCell.domain).colDomain;
        const sortedColumn = pivot.definition.sortedColumn;
        if (sortedColumn?.measure === pivotCell.measure && deepEquals(sortedColumn.domain, colDomain)) {
            return sortedColumn.order;
        }
        return "none";
    }
    // ---------------------------------------------------------------------
    // Private
    // ---------------------------------------------------------------------
    /**
     * Refresh the cache of a pivot
     */
    refreshPivot(pivotId) {
        const pivot = this.getters.getPivot(pivotId);
        pivot.init({ reload: true });
    }
    setupPivot(pivotId, { recreate } = { recreate: false }) {
        const definition = deepCopy(this.getters.getPivotCoreDefinition(pivotId));
        if (!(pivotId in this.pivots)) {
            const Pivot = withPivotPresentationLayer(pivotRegistry.get(definition.type).ui);
            this.pivots[pivotId] = new Pivot(pivotId, this.custom, { definition, getters: this.getters });
        }
        else if (recreate) {
            this.pivots[pivotId].onDefinitionChange(definition);
        }
    }
    _getUnusedPivots() {
        if (this.unusedPivots !== undefined) {
            return this.unusedPivots;
        }
        const unusedPivots = new Set(this.getters.getPivotIds());
        for (const sheetId of this.getters.getSheetIds()) {
            for (const cellId in this.getters.getCells(sheetId)) {
                const position = this.getters.getCellPosition(cellId);
                const pivotId = this.getPivotIdFromPosition(position);
                if (pivotId) {
                    unusedPivots.delete(pivotId);
                    if (!unusedPivots.size) {
                        this.unusedPivots = [];
                        return [];
                    }
                }
            }
        }
        this.unusedPivots = [...unusedPivots];
        return this.unusedPivots;
    }
}

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

const autofillRulesRegistry = new Registry();
const numberPostfixRegExp = /(\d+)$/;
const stringPrefixRegExp = /^(.*\D+)/;
const alphaNumericValueRegExp = /^(.*\D+)(\d+)$/;
const leadingZerosRegex = /^0*/;
/**
 * Get the consecutive evaluated cells that can pass the filter function (e.g. certain type filter).
 * Return the one which contains the given cell
 */
function getGroup(cell, cells, filter) {
    let group = [];
    let found = false;
    for (const x of cells) {
        if (x === cell) {
            found = true;
        }
        const cellValue = x === undefined || x.isFormula
            ? undefined
            : evaluateLiteral(x, { locale: DEFAULT_LOCALE, format: x.format });
        if (cellValue && filter(cellValue)) {
            group.push(cellValue);
        }
        else {
            if (found) {
                return group;
            }
            group = [];
        }
    }
    return group;
}
/**
 * Get the average steps between numbers
 */
function getAverageIncrement(group) {
    const averages = [];
    let last = group[0];
    for (let i = 1; i < group.length; i++) {
        const current = group[i];
        averages.push(current - last);
        last = current;
    }
    return averages.reduce((a, b) => a + b, 0) / averages.length;
}
/**
 * Get the step for a group
 */
function calculateIncrementBasedOnGroup(group) {
    let increment = 1;
    if (group.length >= 2) {
        increment = getAverageIncrement(group) * group.length;
    }
    return increment;
}
/**
 * Iterates on a list of date intervals.
 * if every interval is the same, return the interval
 * Otherwise return undefined
 *
 */
function getEqualInterval(intervals) {
    if (intervals.length < 2) {
        return intervals[0] || { years: 0, months: 0, days: 0 };
    }
    const equal = intervals.every((interval) => interval.years === intervals[0].years &&
        interval.months === intervals[0].months &&
        interval.days === intervals[0].days);
    return equal ? intervals[0] : undefined;
}
/**
 * Based on a group of dates, calculate the increment that should be applied
 * to the next date.
 *
 * This will compute the date difference in calendar terms (years, months, days)
 * In order to make abstraction of leap years and months with different number of days.
 *
 * In case the dates are not equidistant in calendar terms, no rule can be extrapolated
 * In case of equidistant dates, we either have in that order:
 *  - exact date interval (e.g. +n year OR +n month OR +n day) in which case we increment by the same interval
 *  - exact day interval (e.g. +n days) in which case we increment by the same day interval
 *  - equidistant dates but not the same interval, in which case we return increment of the same interval
 *
 * */
function calculateDateIncrementBasedOnGroup(group) {
    if (group.length < 2) {
        return 1;
    }
    const jsDates = group.map((date) => toJsDate(date, DEFAULT_LOCALE));
    const datesIntervals = getDateIntervals(jsDates);
    const datesEquidistantInterval = getEqualInterval(datesIntervals);
    if (datesEquidistantInterval === undefined) {
        // dates are not equidistant in terms of years, months or days, thus no rule can be extrapolated
        return undefined;
    }
    // The dates are apart by an exact interval of years, months or days
    // but not a combination of them
    const exactDateInterval = Object.values(datesEquidistantInterval).filter((value) => value !== 0).length === 1;
    const isSameDay = Object.values(datesEquidistantInterval).every((el) => el === 0); // handles time values (strict decimals)
    if (!exactDateInterval || isSameDay) {
        const timeIntervals = jsDates
            .map((date, index) => {
            if (index === 0) {
                return 0;
            }
            const previous = jsDates[index - 1];
            return Math.floor(date.getTime()) - Math.floor(previous.getTime());
        })
            .slice(1);
        const equidistantDates = timeIntervals.every((interval) => interval === timeIntervals[0]);
        if (equidistantDates) {
            return group.length * (group[1] - group[0]);
        }
    }
    return {
        years: datesEquidistantInterval.years * group.length,
        months: datesEquidistantInterval.months * group.length,
        days: datesEquidistantInterval.days * group.length,
    };
}
autofillRulesRegistry
    .add("simple_value_copy", {
    condition: (cell, cells) => {
        return (cells.length === 1 && !cell.isFormula && !(cell.format && isDateTimeFormat(cell.format)));
    },
    generateRule: () => {
        return { type: "COPY_MODIFIER" };
    },
    sequence: 10,
})
    .add("increment_alphanumeric_value", {
    condition: (cell) => !cell.isFormula &&
        evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text &&
        alphaNumericValueRegExp.test(cell.content),
    generateRule: (cell, cells, direction) => {
        const numberPostfix = parseInt(cell.content.match(numberPostfixRegExp)[0]);
        const prefix = cell.content.match(stringPrefixRegExp)[0];
        const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.text &&
            alphaNumericValueRegExp.test(evaluatedCell.value))
            // get consecutive alphanumeric cells, no matter what the prefix is
            .filter((cell) => prefix === (cell.value ?? "").toString().match(stringPrefixRegExp)[0])
            .map((cell) => (cell.value ?? "").toString().match(numberPostfixRegExp)[0]);
        // find the length of number with the most leading zeros
        const mostLeadingZeros = group.reduce((candidate, current) => {
            const currentLength = current.match(leadingZerosRegex)[0].length;
            return currentLength > candidate[1] ? [current, currentLength] : candidate;
        }, [group[0], 0]);
        const numberPostfixLength = mostLeadingZeros[1] ? mostLeadingZeros[0].length : 0;
        let increment = calculateIncrementBasedOnGroup(group.map((x) => parseInt(x)));
        if (["up", "left"].includes(direction) && group.length === 1) {
            increment = -increment;
        }
        return {
            type: "ALPHANUMERIC_INCREMENT_MODIFIER",
            prefix,
            current: numberPostfix,
            increment,
            numberPostfixLength,
        };
    },
    sequence: 15,
})
    .add("copy_text", {
    condition: (cell) => !cell.isFormula &&
        evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text,
    generateRule: () => {
        return { type: "COPY_MODIFIER" };
    },
    sequence: 20,
})
    .add("update_formula", {
    condition: (cell) => cell.isFormula,
    generateRule: (_, cells) => {
        return { type: "FORMULA_MODIFIER", increment: cells.length, current: 0 };
    },
    sequence: 30,
})
    .add("increment_dates", {
    condition: (cell, cells) => {
        return (!cell.isFormula &&
            evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number &&
            !!cell.format &&
            isDateTimeFormat(cell.format));
    },
    generateRule: (cell, cells) => {
        const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
            !!evaluatedCell.format &&
            isDateTimeFormat(evaluatedCell.format)).map((cell) => Number(cell.value));
        const increment = calculateDateIncrementBasedOnGroup(group);
        if (increment === undefined) {
            return { type: "COPY_MODIFIER" };
        }
        /**  requires to detect the current date (requires to be an integer value  with the right format)
         * detect  if year  or if month or if day then extrapolate increment required (+1 month, +1 year + 1 day)
         */
        const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
        if (typeof increment === "object") {
            return {
                type: "DATE_INCREMENT_MODIFIER",
                increment,
                current: evaluation.type === CellValueType.number ? evaluation.value : 0,
            };
        }
        return {
            type: "INCREMENT_MODIFIER",
            increment,
            current: evaluation.type === CellValueType.number ? evaluation.value : 0,
        };
    },
    sequence: 25,
})
    .add("increment_number", {
    condition: (cell) => !cell.isFormula &&
        evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number,
    generateRule: (cell, cells, direction) => {
        const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
            !isDateTimeFormat(evaluatedCell.format || "")).map((cell) => Number(cell.value));
        let increment = calculateIncrementBasedOnGroup(group);
        if (["up", "left"].includes(direction) && group.length === 1) {
            increment = -increment;
        }
        const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
        return {
            type: "INCREMENT_MODIFIER",
            increment,
            current: evaluation.type === CellValueType.number ? evaluation.value : 0,
        };
    },
    sequence: 40,
});
/**
 * Returns the date intervals between consecutive dates of an array
 * in the format of { years: number, months: number, days: number }
 *
 * The split is necessary to make abstraction of leap years and
 * months with different number of days.
 *
 * @param dates
 */
function getDateIntervals(dates) {
    if (dates.length < 2) {
        return [{ years: 0, months: 0, days: 0 }];
    }
    const res = dates.map((date, index) => {
        if (index === 0) {
            return { years: 0, months: 0, days: 0 };
        }
        const previous = DateTime.fromTimestamp(dates[index - 1].getTime());
        const years = getTimeDifferenceInWholeYears(previous, date);
        const months = getTimeDifferenceInWholeMonths(previous, date) % 12;
        previous.setFullYear(previous.getFullYear() + years);
        previous.setMonth(previous.getMonth() + months);
        const days = getTimeDifferenceInWholeDays(previous, date);
        return {
            years,
            months,
            days,
        };
    });
    return res.slice(1);
}

/**
 * This plugin manage the autofill.
 *
 * The way it works is the next one:
 * For each line (row if the direction is left/right, col otherwise), we create
 * a "AutofillGenerator" object which is used to compute the cells to
 * autofill.
 *
 * When we need to autofill a cell, we compute the origin cell in the source.
 *  EX: from A1:A2, autofill A3->A6.
 *      Target | Origin cell
 *        A3   |   A1
 *        A4   |   A2
 *        A5   |   A1
 *        A6   |   A2
 * When we have the origin, we take the associated cell in the AutofillGenerator
 * and we apply the modifier (AutofillModifier) associated to the content of the
 * cell.
 */
/**
 * This class is used to generate the next values to autofill.
 * It's done from a selection (the source) and describe how the next values
 * should be computed.
 */
class AutofillGenerator {
    cells;
    getters;
    index = 0;
    direction;
    constructor(cells, getters, direction) {
        this.cells = cells;
        this.getters = getters;
        this.direction = direction;
    }
    /**
     * Get the next value to autofill
     */
    next() {
        const genCell = this.cells[this.index++ % this.cells.length];
        const rule = genCell.rule;
        const { cellData, tooltip } = autofillModifiersRegistry
            .get(rule.type)
            .apply(rule, genCell.data, this.getters, this.direction);
        return {
            cellData,
            tooltip,
            origin: {
                col: genCell.data.col,
                row: genCell.data.row,
            },
        };
    }
}
/**
 * Autofill Plugin
 *
 */
class AutofillPlugin extends UIPlugin {
    static layers = ["Autofill"];
    static getters = ["getAutofillTooltip"];
    autofillZone;
    steps;
    lastCellSelected = {};
    direction;
    tooltip;
    // ---------------------------------------------------------------------------
    // Command Handling
    // ---------------------------------------------------------------------------
    allowDispatch(cmd) {
        switch (cmd.type) {
            case "AUTOFILL_SELECT":
                const sheetId = this.getters.getActiveSheetId();
                this.lastCellSelected.col =
                    cmd.col === -1
                        ? this.lastCellSelected.col
                        : clip(cmd.col, 0, this.getters.getNumberCols(sheetId));
                this.lastCellSelected.row =
                    cmd.row === -1
                        ? this.lastCellSelected.row
                        : clip(cmd.row, 0, this.getters.getNumberRows(sheetId));
                if (this.lastCellSelected.col !== undefined && this.lastCellSelected.row !== undefined) {
                    return "Success" /* CommandResult.Success */;
                }
                return "InvalidAutofillSelection" /* CommandResult.InvalidAutofillSelection */;
        }
        return "Success" /* CommandResult.Success */;
    }
    handle(cmd) {
        switch (cmd.type) {
            case "AUTOFILL":
                this.autofill(true);
                break;
            case "AUTOFILL_SELECT":
                this.select(cmd.col, cmd.row);
                break;
            case "AUTOFILL_AUTO":
                this.autofillAuto();
                break;
        }
    }
    // ---------------------------------------------------------------------------
    // 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;
        const autofillCellsData = [];
        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++) {
                        autofillCellsData.push(this.computeNewCell(generator, col, row));
                    }
                }
                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--) {
                        autofillCellsData.push(this.computeNewCell(generator, col, row));
                    }
                }
                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--) {
                        autofillCellsData.push(this.computeNewCell(generator, col, row));
                    }
                }
                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++) {
                        autofillCellsData.push(this.computeNewCell(generator, col, row));
                    }
                }
                break;
        }
        if (apply) {
            const bordersZones = {};
            const cfNewRanges = {};
            const dvNewZones = {};
            const sheetId = this.getters.getActiveSheetId();
            for (const data of autofillCellsData) {
                this.collectBordersData(data, bordersZones);
                this.autofillMerge(sheetId, data);
                this.autofillCell(sheetId, data);
                this.collectConditionalFormatsData(sheetId, data, cfNewRanges);
                this.collectDataValidationsData(sheetId, data, dvNewZones);
            }
            this.autofillBorders(sheetId, bordersZones);
            this.autofillConditionalFormats(sheetId, cfNewRanges);
            this.autofillDataValidations(sheetId, dvNewZones);
            this.autofillZone = undefined;
            this.selection.resizeAnchorZone(this.direction, this.steps);
            this.lastCellSelected = {};
            this.direction = undefined;
            this.steps = 0;
            this.tooltip = undefined;
        }
    }
    collectBordersData(data, bordersPositions) {
        const key = JSON.stringify(data.border);
        if (!(key in bordersPositions)) {
            bordersPositions[key] = [];
        }
        bordersPositions[key].push(positionToZone({ col: data.col, row: data.row }));
    }
    collectConditionalFormatsData(sheetId, data, cfNewRanges) {
        const { originCol, originRow, col, row } = data;
        const cfsAtOrigin = this.getters.getRulesByCell(sheetId, originCol, originRow);
        const xc = toXC(col, row);
        for (const cf of cfsAtOrigin) {
            if (!(cf.id in cfNewRanges)) {
                cfNewRanges[cf.id] = [];
            }
            cfNewRanges[cf.id].push(xc);
        }
    }
    collectDataValidationsData(sheetId, data, dvNewZones) {
        const { originCol, originRow, col, row } = data;
        const cellPosition = { sheetId, col: originCol, row: originRow };
        const dvsAtOrigin = this.getters.getValidationRuleForCell(cellPosition);
        if (!dvsAtOrigin) {
            return;
        }
        if (!(dvsAtOrigin.id in dvNewZones)) {
            dvNewZones[dvsAtOrigin.id] = [];
        }
        dvNewZones[dvsAtOrigin.id].push(positionToZone({ col, row }));
    }
    autofillCell(sheetId, data) {
        this.dispatch("UPDATE_CELL", {
            sheetId,
            col: data.col,
            row: data.row,
            content: data.content || "",
            style: data.style || null,
            format: data.format || "",
        });
        // Still usefull in odoo ATM to autofill field sync
        this.dispatch("AUTOFILL_CELL", data);
    }
    autofillBorders(sheetId, bordersPositions) {
        for (const stringifiedBorder in bordersPositions) {
            const border = stringifiedBorder === "undefined" ? undefined : JSON.parse(stringifiedBorder);
            this.dispatch("SET_BORDERS_ON_TARGET", {
                sheetId,
                border,
                target: recomputeZones(bordersPositions[stringifiedBorder]),
            });
        }
    }
    autofillConditionalFormats(sheetId, cfNewRanges) {
        for (const cfId in cfNewRanges) {
            const changes = cfNewRanges[cfId];
            const cf = this.getters.getConditionalFormats(sheetId).find((cf) => cf.id === cfId);
            if (!cf) {
                continue;
            }
            const newCfRanges = this.getters.getAdaptedCfRanges(sheetId, cf, changes.map(toZone), []);
            if (newCfRanges) {
                this.dispatch("ADD_CONDITIONAL_FORMAT", {
                    cf: {
                        id: cf.id,
                        rule: cf.rule,
                        stopIfTrue: cf.stopIfTrue,
                    },
                    ranges: newCfRanges,
                    sheetId,
                });
            }
        }
    }
    autofillDataValidations(sheetId, dvNewZones) {
        for (const dvId in dvNewZones) {
            const changes = dvNewZones[dvId];
            const dvOrigin = this.getters.getDataValidationRule(sheetId, dvId);
            if (!dvOrigin) {
                continue;
            }
            const dvRangesXcs = dvOrigin.ranges.map((range) => range.zone);
            const newDvRanges = recomputeZones(dvRangesXcs.concat(changes), []);
            this.dispatch("ADD_DATA_VALIDATION_RULE", {
                rule: dvOrigin,
                ranges: newDvRanges.map((zone) => this.getters.getRangeDataFromZone(sheetId, zone)),
                sheetId,
            });
        }
    }
    /**
     * 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 activePosition = this.getters.getActivePosition();
        const table = this.getters.getTable(activePosition);
        let autofillRow = table ? table.range.zone.bottom : this.getAutofillAutoLastRow();
        // Stop autofill at the next non-empty cell
        const selection = this.getters.getSelectedZone();
        for (let row = selection.bottom + 1; row <= autofillRow; row++) {
            if (this.getters.getEvaluatedCell({ ...activePosition, row }).type !== CellValueType.empty) {
                autofillRow = row - 1;
                break;
            }
        }
        if (autofillRow > selection.bottom) {
            this.select(activePosition.col, autofillRow);
            this.autofill(true);
        }
    }
    getAutofillAutoLastRow() {
        const zone = this.getters.getSelectedZone();
        const sheetId = this.getters.getActiveSheetId();
        let col = zone.left;
        let row = zone.bottom;
        if (col > 0) {
            let leftPosition = { sheetId, col: col - 1, row };
            while (this.getters.getEvaluatedCell(leftPosition).type !== CellValueType.empty) {
                row += 1;
                leftPosition = { sheetId, col: col - 1, row };
            }
        }
        if (row === zone.bottom) {
            col = zone.right;
            if (col <= this.getters.getNumberCols(sheetId)) {
                let rightPosition = { sheetId, col: col + 1, row };
                while (this.getters.getEvaluatedCell(rightPosition).type !== CellValueType.empty) {
                    row += 1;
                    rightPosition = { sheetId, col: col + 1, row };
                }
            }
        }
        return row - 1;
    }
    /**
     * Generate the next cell
     */
    computeNewCell(generator, col, row) {
        const { cellData, tooltip, origin } = generator.next();
        const { content, style, border, format } = cellData;
        this.tooltip = tooltip;
        return {
            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 (const 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 (const cellData of cellsData) {
            let rule = { type: "COPY_MODIFIER" };
            if (cellData && cellData.cell) {
                const newRule = this.getRule(cellData.cell, cells);
                rule = newRule || rule;
            }
            const border = this.getters.getCellBorder(cellData) || undefined;
            nextCells.push({
                data: { ...cellData, border },
                rule,
            });
        }
        return new AutofillGenerator(nextCells, this.getters, this.direction);
    }
    saveZone(top, bottom, left, right) {
        this.autofillZone = { top, bottom, left, right };
    }
    /**
     * Compute the direction of the autofill from the last selected zone and
     * a given cell (col, row)
     */
    getDirection(col, row) {
        const source = this.getters.getSelectedZone();
        const position = {
            up: { number: source.top - row, value: "up" /* DIRECTION.UP */ },
            down: { number: row - source.bottom, value: "down" /* DIRECTION.DOWN */ },
            left: { number: source.left - col, value: "left" /* DIRECTION.LEFT */ },
            right: { number: col - source.right, value: "right" /* DIRECTION.RIGHT */ },
        };
        if (Object.values(position)
            .map((x) => (x.number > 0 ? 1 : 0))
            .reduce((acc, value) => acc + value) === 1) {
            return Object.values(position).find((x) => (x.number > 0 ? 1 : 0)).value;
        }
        const first = position.up.number > 0 ? "up" : "down";
        const second = position.left.number > 0 ? "left" : "right";
        return Math.abs(position[first].number) >= Math.abs(position[second].number)
            ? position[first].value
            : position[second].value;
    }
    autofillMerge(sheetId, data) {
        const { originCol, originRow, col, row } = data;
        const position = { sheetId, col, row };
        const originPosition = { sheetId, col: originCol, row: originRow };
        if (this.getters.isInMerge(position) && !this.getters.isInMerge(originPosition)) {
            const zone = this.getters.getMerge(position);
            if (zone) {
                this.dispatch("REMOVE_MERGE", {
                    sheetId,
                    target: [zone],
                });
            }
        }
        const originMerge = this.getters.getMerge(originPosition);
        if (originMerge?.left === originCol && originMerge?.top === originRow) {
            this.dispatch("ADD_MERGE", {
                sheetId,
                target: [
                    {
                        top: row,
                        bottom: row + originMerge.bottom - originMerge.top,
                        left: col,
                        right: col + originMerge.right - originMerge.left,
                    },
                ],
            });
        }
    }
    // ---------------------------------------------------------------------------
    // Grid rendering
    // ---------------------------------------------------------------------------
    drawLayer(renderingContext) {
        if (!this.autofillZone) {
            return;
        }
        const { ctx, thinLineWidth } = renderingContext;
        const { x, y, width, height } = this.getters.getVisibleRect(this.autofillZone);
        if (width > 0 && height > 0) {
            ctx.strokeStyle = "black";
            ctx.lineWidth = thinLineWidth;
            ctx.setLineDash([3]);
            ctx.strokeRect(x, y, width, height);
            ctx.setLineDash([]);
        }
    }
}

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

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

class SpecificRangeTransformRegistry extends Registry {
    add(cmdType, fn) {
        super.add(cmdType, fn);
        return this;
    }
    replace(cmdType, fn) {
        super.replace(cmdType, fn);
        return this;
    }
    get(cmdType) {
        return this.content[cmdType];
    }
}
const specificRangeTransformRegistry = new SpecificRangeTransformRegistry();

/*
 * This file contains the specifics transformations
 */
otRegistry.addTransformation("ADD_COLUMNS_ROWS", ["ADD_COLUMNS_ROWS"], addHeadersTransformation);
otRegistry.addTransformation("REMOVE_COLUMNS_ROWS", ["ADD_COLUMNS_ROWS"], addHeadersTransformation);
otRegistry.addTransformation("DELETE_SHEET", ["MOVE_RANGES"], transformTargetSheetId);
otRegistry.addTransformation("DELETE_FIGURE", ["UPDATE_FIGURE", "UPDATE_CHART", "UPDATE_CAROUSEL"], updateChartFigure);
otRegistry.addTransformation("DELETE_CHART", ["UPDATE_CHART"], updateChartOnChartDelete);
otRegistry.addTransformation("DELETE_CHART", ["UPDATE_CAROUSEL"], updateCarouselOnChartDelete);
otRegistry.addTransformation("CREATE_SHEET", ["CREATE_SHEET"], createSheetTransformation);
otRegistry.addTransformation("ADD_MERGE", ["ADD_MERGE", "REMOVE_MERGE"], mergeTransformation);
otRegistry.addTransformation("ADD_COLUMNS_ROWS", ["FREEZE_COLUMNS", "FREEZE_ROWS"], freezeTransformation);
otRegistry.addTransformation("REMOVE_COLUMNS_ROWS", ["FREEZE_COLUMNS", "FREEZE_ROWS"], freezeTransformation);
otRegistry.addTransformation("ADD_COLUMNS_ROWS", ["UPDATE_TABLE"], updateTableTransformation);
otRegistry.addTransformation("REMOVE_COLUMNS_ROWS", ["UPDATE_TABLE"], updateTableTransformation);
otRegistry.addTransformation("REMOVE_TABLE_STYLE", ["CREATE_TABLE", "UPDATE_TABLE"], removeTableStyleTransform);
otRegistry.addTransformation("ADD_COLUMNS_ROWS", ["GROUP_HEADERS", "UNGROUP_HEADERS", "FOLD_HEADER_GROUP", "UNFOLD_HEADER_GROUP"], groupHeadersTransformation);
otRegistry.addTransformation("REMOVE_COLUMNS_ROWS", ["GROUP_HEADERS", "UNGROUP_HEADERS", "FOLD_HEADER_GROUP", "UNFOLD_HEADER_GROUP"], groupHeadersTransformation);
otRegistry.addTransformation("REMOVE_PIVOT", ["RENAME_PIVOT", "DUPLICATE_PIVOT", "INSERT_PIVOT", "UPDATE_PIVOT"], pivotRemovedTransformation);
otRegistry.addTransformation("DELETE_SHEET", ["ADD_PIVOT", "UPDATE_PIVOT"], pivotDeletedSheetTransformation);
otRegistry.addTransformation("ADD_COLUMNS_ROWS", ["ADD_PIVOT", "UPDATE_PIVOT"], pivotZoneTransformation);
otRegistry.addTransformation("REMOVE_COLUMNS_ROWS", ["ADD_PIVOT", "UPDATE_PIVOT"], pivotZoneTransformation);
function pivotZoneTransformation(toTransform, executed) {
    if (toTransform.pivot.type !== "SPREADSHEET") {
        return toTransform;
    }
    if (toTransform.pivot.dataSet?.sheetId !== executed.sheetId) {
        return toTransform;
    }
    const newZone = transformZone(toTransform.pivot.dataSet.zone, executed);
    const dataSet = newZone ? { ...toTransform.pivot.dataSet, zone: newZone } : undefined;
    return { ...toTransform, pivot: { ...toTransform.pivot, dataSet } };
}
function pivotDeletedSheetTransformation(toTransform, executed) {
    if (toTransform.pivot.type !== "SPREADSHEET") {
        return toTransform;
    }
    if (toTransform.pivot.dataSet?.sheetId === executed.sheetId) {
        return { ...toTransform, pivot: { ...toTransform.pivot, dataSet: undefined } };
    }
    return toTransform;
}
function pivotRemovedTransformation(toTransform, executed) {
    if (toTransform.pivotId === executed.pivotId) {
        return undefined;
    }
    return toTransform;
}
function transformTargetSheetId(toTransform, executed) {
    const deletedSheetId = executed.sheetId;
    if (toTransform.targetSheetId === deletedSheetId || toTransform.sheetId === deletedSheetId) {
        return undefined;
    }
    return toTransform;
}
function updateChartFigure(toTransform, executed) {
    if (toTransform.figureId === executed.figureId) {
        return undefined;
    }
    return toTransform;
}
function updateChartOnChartDelete(toTransform, executed) {
    if (toTransform.chartId === executed.chartId) {
        return undefined;
    }
    return toTransform;
}
function updateCarouselOnChartDelete(toTransform, executed) {
    return {
        ...toTransform,
        definition: {
            ...toTransform.definition,
            items: toTransform.definition.items.filter((item) => !(item.type === "chart" && item.chartId === executed.chartId)),
        },
    };
}
function createSheetTransformation(toTransform, executed) {
    if (toTransform.sheetId === executed.sheetId) {
        toTransform = { ...toTransform, sheetId: `${toTransform.sheetId}~` };
    }
    if (toTransform.name === executed.name) {
        return {
            ...toTransform,
            name: toTransform.name?.match(/\d+/)
                ? toTransform.name.replace(/\d+/, (n) => (parseInt(n) + 1).toString())
                : `${toTransform.name}~`,
            position: toTransform.position + 1,
        };
    }
    return toTransform;
}
function mergeTransformation(toTransform, executed) {
    if (toTransform.sheetId !== executed.sheetId) {
        return toTransform;
    }
    const target = [];
    for (const zone1 of toTransform.target) {
        if (executed.target.every((zone2) => !overlap(zone1, zone2))) {
            target.push(zone1);
        }
    }
    if (target.length) {
        return { ...toTransform, target };
    }
    return undefined;
}
function freezeTransformation(toTransform, executed) {
    if (toTransform.sheetId !== executed.sheetId) {
        return toTransform;
    }
    const dimension = toTransform.type === "FREEZE_COLUMNS" ? "COL" : "ROW";
    if (dimension !== executed.dimension) {
        return toTransform;
    }
    let quantity = toTransform["quantity"];
    if (executed.type === "REMOVE_COLUMNS_ROWS") {
        const executedElements = [...executed.elements].sort((a, b) => b - a);
        for (const removedElement of executedElements) {
            if (quantity > removedElement) {
                quantity--;
            }
        }
    }
    if (executed.type === "ADD_COLUMNS_ROWS") {
        const executedBase = executed.position === "before" ? executed.base - 1 : executed.base;
        quantity = quantity > executedBase ? quantity + executed.quantity : quantity;
    }
    return quantity > 0 ? { ...toTransform, quantity } : undefined;
}
/**
 * Update the zones of an UPDATE_TABLE command if some headers were added/removed
 */
function updateTableTransformation(toTransform, executed) {
    if (toTransform.sheetId !== executed.sheetId) {
        return toTransform;
    }
    const newCmdZone = transformZone(toTransform.zone, executed);
    if (!newCmdZone) {
        return undefined;
    }
    const newTableRange = toTransform.newTableRange
        ? transformRangeData(toTransform.newTableRange, executed)
        : undefined;
    return { ...toTransform, newTableRange, zone: newCmdZone };
}
function removeTableStyleTransform(toTransform, executed) {
    if (toTransform.config?.styleId !== executed.tableStyleId) {
        return toTransform;
    }
    return {
        ...toTransform,
        config: { ...toTransform.config, styleId: DEFAULT_TABLE_CONFIG.styleId },
    };
}
/**
 * Transform ADD_COLUMNS_ROWS command if some headers were added/removed
 */
function addHeadersTransformation(toTransform, executed) {
    if (toTransform.sheetId !== executed.sheetId || toTransform.dimension !== executed.dimension) {
        return toTransform;
    }
    let result = undefined;
    if (executed.type === "REMOVE_COLUMNS_ROWS") {
        result = moveHeaderIndexesOnHeaderDeletion(executed.elements, [toTransform.base])[0];
    }
    else if (executed.type === "ADD_COLUMNS_ROWS") {
        const base = getAddHeaderStartIndex(executed.position, executed.base);
        result = moveHeaderIndexesOnHeaderAddition(base, executed.quantity, [toTransform.base])[0];
    }
    if (result === undefined) {
        return undefined;
    }
    return { ...toTransform, base: result };
}
/**
 * Transform header group command if some headers were added/removed
 */
function groupHeadersTransformation(toTransform, executed) {
    if (toTransform.sheetId !== executed.sheetId || toTransform.dimension !== executed.dimension) {
        return toTransform;
    }
    const elementsToTransform = range(toTransform.start, toTransform.end + 1);
    let results = [];
    if (executed.type === "REMOVE_COLUMNS_ROWS") {
        results = moveHeaderIndexesOnHeaderDeletion(executed.elements, elementsToTransform);
    }
    else if (executed.type === "ADD_COLUMNS_ROWS") {
        const base = getAddHeaderStartIndex(executed.position, executed.base);
        results = moveHeaderIndexesOnHeaderAddition(base, executed.quantity, elementsToTransform);
    }
    if (results.length === 0) {
        return undefined;
    }
    return { ...toTransform, start: Math.min(...results), end: Math.max(...results) };
}

function updateCellCommandAdaptRange(cmd, applyChange) {
    const content = cmd.content && adaptFormulaStringRanges(cmd.sheetId, cmd.content, applyChange);
    return { ...cmd, content };
}
specificRangeTransformRegistry.add("UPDATE_CELL", updateCellCommandAdaptRange);
function addConditionalFormatCommandAdaptRange(cmd, applyChange) {
    const rule = cmd.cf.rule;
    cmd = { ...cmd, cf: { ...cmd.cf } };
    if (rule.type === "CellIsRule") {
        cmd.cf.rule = {
            ...rule,
            values: rule.values.map((val) => adaptFormulaStringRanges(cmd.sheetId, val, applyChange)),
        };
    }
    else if (rule.type === "ColorScaleRule") {
        const { minimum: min, maximum: max, midpoint: mid } = rule;
        cmd.cf.rule = {
            ...rule,
            minimum: {
                ...min,
                value: min.value && adaptFormulaStringRanges(cmd.sheetId, min.value, applyChange),
            },
            maximum: {
                ...max,
                value: max.value && adaptFormulaStringRanges(cmd.sheetId, max.value, applyChange),
            },
            midpoint: mid
                ? { ...mid, value: adaptFormulaStringRanges(cmd.sheetId, mid.value, applyChange) }
                : undefined,
        };
    }
    else if (rule.type === "IconSetRule") {
        const { upperInflectionPoint: uip, lowerInflectionPoint: lip } = rule;
        cmd.cf.rule = {
            ...rule,
            upperInflectionPoint: {
                ...uip,
                value: adaptFormulaStringRanges(cmd.sheetId, uip.value, applyChange),
            },
            lowerInflectionPoint: {
                ...lip,
                value: adaptFormulaStringRanges(cmd.sheetId, lip.value, applyChange),
            },
        };
    }
    else if (rule.type === "DataBarRule") {
        cmd.cf.rule = {
            ...rule,
            rangeValues: rule.rangeValues
                ? adaptStringRange(cmd.sheetId, rule.rangeValues, applyChange).range
                : undefined,
        };
    }
    return cmd;
}
specificRangeTransformRegistry.add("ADD_CONDITIONAL_FORMAT", addConditionalFormatCommandAdaptRange);
function addDataValidationCommandAdaptRange(cmd, applyChange) {
    cmd = { ...cmd, rule: { ...cmd.rule, criterion: { ...cmd.rule.criterion } } };
    cmd.rule.criterion.values = cmd.rule.criterion.values.map((val) => adaptFormulaStringRanges(cmd.sheetId, val, applyChange));
    return cmd;
}
specificRangeTransformRegistry.add("ADD_DATA_VALIDATION_RULE", addDataValidationCommandAdaptRange);
function addPivotCommandAdaptRange(cmd, applyChange) {
    cmd = deepCopy(cmd);
    cmd.pivot.measures.map((measure) => {
        if (measure.computedBy) {
            measure.computedBy.formula = adaptFormulaStringRanges(measure.computedBy.sheetId, measure.computedBy.formula, applyChange);
        }
    });
    return cmd;
}
specificRangeTransformRegistry.add("ADD_PIVOT", addPivotCommandAdaptRange);
specificRangeTransformRegistry.add("UPDATE_PIVOT", addPivotCommandAdaptRange);
specificRangeTransformRegistry.add("CREATE_CHART", updateChartRangesTransformation);
specificRangeTransformRegistry.add("UPDATE_CHART", updateChartRangesTransformation);
function updateChartRangesTransformation(cmd, applyChange) {
    return {
        ...cmd,
        definition: transformDefinition(cmd.sheetId, cmd.definition, applyChange),
    };
}

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

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

class ClientDisconnectedError extends Error {
}
class Session extends EventBus {
    revisions;
    transportService;
    serverRevisionId;
    /**
     * Positions of the others client.
     */
    clients = {};
    clientId = "local";
    /**
     * Id of the server revision
     */
    debouncedMove;
    pendingMessages = [];
    /**
     * Stored position of the client, if session.move is called while the client is not in a session.
     * Will be used to send the position to the server when the client joins.
     */
    awaitingClientPosition;
    waitingAck = false;
    /**
     * Flag used to block all commands when an undo or redo is triggered, until
     * it is accepted on the server
     */
    waitingUndoRedoAck = false;
    isReplayingInitialRevisions = false;
    processedRevisions = new Set();
    lastRevisionMessage = undefined;
    uuidGenerator = new UuidGenerator();
    lastLocalOperation;
    /**
     * Manages the collaboration between multiple users on the same spreadsheet.
     * It can forward local state changes to other users to ensure they all eventually
     * reach the same state.
     * It also manages the positions of each clients in the spreadsheet to provide
     * a visual indication of what other users are doing in the spreadsheet.
     *
     * @param revisions
     * @param transportService communication channel used to send and receive messages
     * between all connected clients
     * @param client the client connected locally
     * @param serverRevisionId
     */
    constructor(revisions, transportService, serverRevisionId = DEFAULT_REVISION_ID) {
        super();
        this.revisions = revisions;
        this.transportService = transportService;
        this.serverRevisionId = serverRevisionId;
        this.debouncedMove = debounce(this._move.bind(this), DEBOUNCE_TIME);
    }
    canApplyOptimisticUpdate() {
        return !this.waitingUndoRedoAck;
    }
    /**
     * Add a new revision to the collaborative session.
     * It will be transmitted to all other connected clients.
     */
    save(rootCommand, commands, changes) {
        if (!commands.length || !changes.length || !this.canApplyOptimisticUpdate())
            return;
        const revision = new Revision(this.uuidGenerator.uuidv4(), this.clientId, commands, rootCommand, changes, Date.now());
        this.revisions.append(revision.id, revision);
        // REQUEST_REDO just repeats the last operation, the
        // last operation is still the same and should not change.
        if (rootCommand.type !== "REQUEST_REDO") {
            this.lastLocalOperation = revision;
        }
        this.trigger("new-local-state-update", { id: revision.id });
        this.sendUpdateMessage({
            type: "REMOTE_REVISION",
            version: MESSAGE_VERSION,
            serverRevisionId: this.serverRevisionId,
            nextRevisionId: revision.id,
            clientId: revision.clientId,
            commands: revision.commands,
        });
    }
    undo(revisionId) {
        this.waitingUndoRedoAck = true;
        this.sendUpdateMessage({
            type: "REVISION_UNDONE",
            version: MESSAGE_VERSION,
            serverRevisionId: this.serverRevisionId,
            nextRevisionId: this.uuidGenerator.uuidv4(),
            undoneRevisionId: revisionId,
        });
    }
    redo(revisionId) {
        this.waitingUndoRedoAck = true;
        this.sendUpdateMessage({
            type: "REVISION_REDONE",
            version: MESSAGE_VERSION,
            serverRevisionId: this.serverRevisionId,
            nextRevisionId: this.uuidGenerator.uuidv4(),
            redoneRevisionId: revisionId,
        });
    }
    /**
     * Notify that the position of the client has changed
     */
    move(position) {
        this.debouncedMove(position);
    }
    join(client) {
        if (client) {
            this.clients[client.id] = client;
            this.clientId = client.id;
        }
        else {
            this.clients["local"] = { id: "local", name: "local" };
            this.clientId = "local";
        }
        this.transportService.onNewMessage(this.clientId, this.onMessageReceived.bind(this));
        if (this.awaitingClientPosition) {
            this._move(this.awaitingClientPosition);
            this.awaitingClientPosition = undefined;
        }
    }
    loadInitialMessages(messages) {
        const start = performance.now();
        const numberOfCommands = messages.reduce((acc, message) => acc + (message.type === "REMOTE_REVISION" ? message.commands.length : 1), 0);
        this.isReplayingInitialRevisions = true;
        for (const message of messages) {
            this.onMessageReceived(message);
        }
        this.isReplayingInitialRevisions = false;
        console.debug("Replayed", numberOfCommands, "commands in", performance.now() - start, "ms");
    }
    /**
     * Notify the server that the user client left the collaborative session
     */
    async leave(data) {
        if (data &&
            Object.keys(this.clients).length === 1 &&
            this.lastRevisionMessage &&
            this.lastRevisionMessage?.type !== "SNAPSHOT_CREATED") {
            await this.snapshot(data());
        }
        delete this.clients[this.clientId];
        this.transportService.leave(this.clientId);
        this.sendToTransport({
            type: "CLIENT_LEFT",
            clientId: this.clientId,
            version: MESSAGE_VERSION,
        });
    }
    /**
     * Send a snapshot of the spreadsheet to the collaboration server
     */
    async snapshot(data) {
        if (this.pendingMessages.length !== 0) {
            return;
        }
        const snapshotId = this.uuidGenerator.uuidv4();
        await this.sendToTransport({
            type: "SNAPSHOT",
            nextRevisionId: snapshotId,
            serverRevisionId: this.serverRevisionId,
            data: { ...data, revisionId: snapshotId },
            version: MESSAGE_VERSION,
        });
    }
    getCurrentClient() {
        return this.getClient(this.clientId);
    }
    getClient(clientId) {
        const client = this.clients[clientId];
        if (!client) {
            throw new ClientDisconnectedError("The client left the session");
        }
        return client;
    }
    getConnectedClients() {
        return new Set(Object.values(this.clients).filter(isDefined));
    }
    getRevisionId() {
        return this.serverRevisionId;
    }
    isFullySynchronized() {
        return this.pendingMessages.length === 0;
    }
    /**
     * Get the last local revision
     * */
    getLastLocalNonEmptyRevision() {
        return this.lastLocalOperation;
    }
    _move(position) {
        // this method could be called before the client joins the session, or after he left (because of the debounce)
        if (!this.clients[this.clientId]) {
            this.awaitingClientPosition = position;
            return;
        }
        const currentPosition = this.clients[this.clientId]?.position;
        if (currentPosition?.col === position.col &&
            currentPosition.row === position.row &&
            currentPosition.sheetId === position.sheetId) {
            return;
        }
        const type = currentPosition ? "CLIENT_MOVED" : "CLIENT_JOINED";
        const client = this.getCurrentClient();
        this.clients[this.clientId] = { ...client, position };
        this.sendToTransport({
            type,
            version: MESSAGE_VERSION,
            client: { ...client, position },
        }).then(() => {
            if (this.pendingMessages.length > 0 && !this.waitingAck) {
                this.sendPendingMessage();
            }
        });
    }
    /**
     * Handles messages received from other clients in the collaborative
     * session.
     */
    onMessageReceived(message) {
        if (this.isAlreadyProcessed(message))
            return;
        if (this.isWrongServerRevisionId(message)) {
            this.trigger("unexpected-revision-id");
            return;
        }
        switch (message.type) {
            case "CLIENT_MOVED":
                this.onClientMoved(message);
                break;
            case "CLIENT_JOINED":
                this.onClientJoined(message);
                break;
            case "CLIENT_LEFT":
                this.onClientLeft(message);
                break;
            case "REVISION_REDONE": {
                this.revisions.redo(message.redoneRevisionId, message.nextRevisionId, message.serverRevisionId);
                this.trigger("revision-redone", {
                    revisionId: message.redoneRevisionId,
                    commands: this.revisions.get(message.redoneRevisionId).commands,
                });
                break;
            }
            case "REVISION_UNDONE":
                this.revisions.undo(message.undoneRevisionId, message.nextRevisionId, message.serverRevisionId);
                this.trigger("revision-undone", {
                    revisionId: message.undoneRevisionId,
                    commands: this.revisions.get(message.undoneRevisionId).commands,
                });
                break;
            case "REMOTE_REVISION":
                const { clientId, commands, timestamp } = message;
                const revision = new Revision(message.nextRevisionId, clientId, commands, undefined, undefined, timestamp);
                if (revision.clientId !== this.clientId) {
                    this.revisions.insert(revision.id, revision, message.serverRevisionId);
                    const pendingCommands = this.pendingMessages
                        .filter((msg) => msg.type === "REMOTE_REVISION")
                        .map((msg) => msg.commands)
                        .flat();
                    this.trigger("remote-revision-received", {
                        commands: transformAll(commands, pendingCommands),
                    });
                }
                break;
            case "SNAPSHOT_CREATED": {
                const revision = new Revision(message.nextRevisionId, "server", [], undefined, undefined, Date.now());
                this.revisions.insert(revision.id, revision, message.serverRevisionId);
                this.dropPendingHistoryMessages();
                this.trigger("snapshot");
                this.lastLocalOperation = undefined;
                break;
            }
        }
        this.acknowledge(message);
        this.trigger("collaborative-event-received");
    }
    onClientMoved(message) {
        if (message.client.id !== this.clientId) {
            this.clients[message.client.id] = message.client;
        }
    }
    /**
     * Register the new client and send your
     * own position back.
     */
    onClientJoined(message) {
        if (message.client.id !== this.clientId) {
            this.clients[message.client.id] = message.client;
            const client = this.clients[this.clientId];
            if (client) {
                const { position } = client;
                if (position) {
                    this.sendToTransport({
                        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();
    }
    async sendToTransport(message) {
        // wrap in an async function to ensure it returns a promise
        return this.transportService.sendMessage(message);
    }
    /**
     * 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.sendToTransport({
            ...message,
            serverRevisionId: this.serverRevisionId,
        }).catch((e) => {
            if (!(e instanceof ClientDisconnectedError)) {
                throw e.cause || e;
            }
            this.waitingAck = false;
        });
    }
    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.lastRevisionMessage = message;
                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");
    }
}

class CollaborativePlugin extends UIPlugin {
    static getters = [
        "getClientsToDisplay",
        "getClient",
        "getCurrentClient",
        "getConnectedClients",
        "isFullySynchronized",
    ];
    static layers = ["Selection"];
    colors = new AlternatingColorMap(12);
    session;
    constructor(config) {
        super(config);
        this.session = config.session;
    }
    isPositionValid(position) {
        return (position.row < this.getters.getNumberRows(position.sheetId) &&
            position.col < this.getters.getNumberCols(position.sheetId));
    }
    getClient(clientId) {
        return this.session.getClient(clientId);
    }
    getCurrentClient() {
        return this.session.getCurrentClient();
    }
    getConnectedClients() {
        return [...this.session.getConnectedClients()].map((client) => {
            return { ...client, color: this.colors.get(client.id) };
        });
    }
    isFullySynchronized() {
        return this.session.isFullySynchronized();
    }
    /**
     * Get the list of others connected clients which are present in the same sheet
     * and with a valid position
     */
    getClientsToDisplay() {
        try {
            this.getters.getCurrentClient();
        }
        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.getCurrentClient().id &&
                client.position &&
                client.position.sheetId === sheetId &&
                this.isPositionValid(client.position)) {
                clients.push({ ...client, position: client.position });
            }
        }
        return clients;
    }
    drawLayer(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 */
            ctx.fillStyle = `${color}10`;
            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}`;
        }
    }
}

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

class FormatPlugin extends UIPlugin {
    // ---------------------------------------------------------------------------
    // Command Handling
    // ---------------------------------------------------------------------------
    handle(cmd) {
        switch (cmd.type) {
            case "SET_DECIMAL":
                this.setDecimal(cmd.sheetId, cmd.target, cmd.step);
                break;
            case "SET_FORMATTING_WITH_PIVOT": {
                this.setContextualFormat(cmd.sheetId, cmd.target, cmd.format);
                break;
            }
        }
    }
    setContextualFormat(sheetId, zones, format) {
        const measurePositions = [];
        const measuresByPivotId = {};
        for (const zone of recomputeZones(zones)) {
            for (let col = zone.left; col <= zone.right; col++) {
                for (let row = zone.top; row <= zone.bottom; row++) {
                    const position = { sheetId, col, row };
                    const pivotCell = this.getters.getPivotCellFromPosition(position);
                    if (this.isSpilledPivotValueFormula(position, pivotCell)) {
                        measurePositions.push(position);
                        const pivotId = this.getters.getPivotIdFromPosition(position) || "";
                        measuresByPivotId[pivotId] ??= new Set();
                        measuresByPivotId[pivotId].add(pivotCell.measure);
                    }
                }
            }
        }
        const measureZones = recomputeZones(measurePositions.map(positionToZone));
        for (const pivotId in measuresByPivotId) {
            const measures = measuresByPivotId[pivotId];
            const pivotDefinition = this.getters.getPivotCoreDefinition(pivotId);
            this.dispatch("UPDATE_PIVOT", {
                pivotId,
                pivot: {
                    ...pivotDefinition,
                    measures: pivotDefinition.measures.map((measure) => {
                        if (measures.has(measure.id)) {
                            return { ...measure, format };
                        }
                        return measure;
                    }),
                },
            });
        }
        this.dispatch("SET_FORMATTING", {
            sheetId,
            target: measureZones,
            format: "",
        });
        this.dispatch("SET_FORMATTING", {
            sheetId,
            target: recomputeZones(zones, measureZones),
            format,
        });
    }
    isSpilledPivotValueFormula(position, pivotCell) {
        const cell = this.getters.getCell(position);
        return pivotCell.type === "VALUE" && !cell?.isFormula;
    }
    /**
     * This function allows to adjust the quantity of decimal places after a decimal
     * point on cells containing number value. It does this by changing the cells
     * format. Values aren't modified.
     *
     * The change of the decimal quantity is done one by one, the sign of the step
     * variable indicates whether we are increasing or decreasing.
     *
     * If several cells are in the zone, each cell's format will be individually
     * evaluated and updated with the number type.
     */
    setDecimal(sheetId, zones, step) {
        const positionsByFormat = {};
        // Find the each cell with a number value and get the format
        for (const zone of recomputeZones(zones)) {
            for (const position of positions(zone)) {
                const numberFormat = this.getCellNumberFormat({ sheetId, ...position });
                if (numberFormat !== undefined) {
                    // Depending on the step sign, increase or decrease the decimal representation
                    // of the format
                    const newFormat = changeDecimalPlaces(numberFormat, step);
                    positionsByFormat[newFormat] = positionsByFormat[newFormat] || [];
                    positionsByFormat[newFormat].push(position);
                }
            }
        }
        // consolidate all positions with the same format in bigger zones
        for (const newFormat in positionsByFormat) {
            const zones = recomputeZones(positionsByFormat[newFormat].map((position) => positionToZone(position)));
            this.setContextualFormat(sheetId, zones, newFormat);
        }
    }
    /**
     * Take a range of cells and return the format of the first cell containing a
     * number value. Returns a default format if the cell hasn't format. Returns
     * undefined if no number value in the range.
     */
    getCellNumberFormat(position) {
        for (const pos of [position]) {
            const cell = this.getters.getEvaluatedCell(pos);
            if (cell.type === CellValueType.number &&
                !(cell.format && isDateTimeFormat(cell.format)) // reject dates
            ) {
                return cell.format || createDefaultFormat(cell.value);
            }
        }
        return undefined;
    }
}

class GeoFeaturePlugin extends UIPlugin {
    static getters = [
        "getGeoJsonFeatures",
        "geoFeatureNameToId",
        "getGeoChartAvailableRegions",
    ];
    geoJsonService;
    geoJsonCache = {};
    constructor(config) {
        super(config);
        this.geoJsonService = config.external.geoJsonService;
    }
    getGeoChartAvailableRegions() {
        if (!this.geoJsonService) {
            console.error("No geoJsonService provided to the model");
            return [];
        }
        return this.geoJsonService.getAvailableRegions() || [];
    }
    getGeoJsonFeatures(region) {
        if (!this.geoJsonService) {
            console.error("No geoJsonService provided to the model");
            return;
        }
        const cachedGeoJson = this.geoJsonCache[region];
        if (cachedGeoJson instanceof Promise) {
            return undefined;
        }
        if (cachedGeoJson !== undefined) {
            return cachedGeoJson ?? undefined;
        }
        this.geoJsonCache[region] = new Promise(async (resolve) => {
            const json = await this.geoJsonService?.getTopoJson(region);
            this.geoJsonCache[region] = this.convertToGeoJson(json);
            this.dispatch("EVALUATE_CHARTS");
            resolve();
        });
        return undefined;
    }
    geoFeatureNameToId(region, featureName) {
        if (!this.geoJsonService) {
            console.error("No geoJsonService provided to the model");
            return;
        }
        return this.geoJsonService.geoFeatureNameToId(region, featureName);
    }
    convertToGeoJson(json) {
        if (!json) {
            return null;
        }
        // TopoJSON
        if (json.type === "Topology") {
            const features = window.ChartGeo.topojson.feature(json, Object.values(json.objects)[0]);
            return features.type === "FeatureCollection" ? features.features : [features];
        }
        // GeoJSON
        else if (json.type === "FeatureCollection") {
            return json.features;
        }
        throw new Error("Invalid TopoJSON");
    }
}

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

class InsertPivotPlugin extends UIPlugin {
    static getters = [];
    allowDispatch(cmd) {
        switch (cmd.type) {
            case "DUPLICATE_PIVOT_IN_NEW_SHEET":
                if (!this.getters.isExistingPivot(cmd.pivotId)) {
                    return "PivotIdNotFound" /* CommandResult.PivotIdNotFound */;
                }
                if (!this.getters.getPivot(cmd.pivotId).isValid()) {
                    return "PivotInError" /* CommandResult.PivotInError */;
                }
                break;
        }
        return "Success" /* CommandResult.Success */;
    }
    handle(cmd) {
        switch (cmd.type) {
            case "INSERT_NEW_PIVOT":
                this.insertNewPivot(cmd.pivotId, cmd.newSheetId);
                break;
            case "DUPLICATE_PIVOT_IN_NEW_SHEET":
                this.duplicatePivotInNewSheet(cmd.pivotId, cmd.newPivotId, cmd.newSheetId);
                break;
            case "INSERT_PIVOT_WITH_TABLE":
                this.insertPivotWithTable(cmd.sheetId, cmd.col, cmd.row, cmd.pivotId, cmd.table, cmd.pivotMode);
                break;
            case "SPLIT_PIVOT_FORMULA":
                this.splitPivotFormula(cmd.sheetId, cmd.col, cmd.row, cmd.pivotId);
        }
    }
    insertNewPivot(pivotId, sheetId) {
        if (getZoneArea(this.getters.getSelectedZone()) === 1) {
            this.selection.selectTableAroundSelection();
        }
        const currentSheetId = this.getters.getActiveSheetId();
        this.dispatch("ADD_PIVOT", {
            pivotId,
            pivot: {
                dataSet: {
                    zone: this.getters.getSelectedZone(),
                    sheetId: currentSheetId,
                },
                columns: [],
                rows: [],
                measures: [],
                name: _t("New pivot"),
                type: "SPREADSHEET",
            },
        });
        const position = this.getters.getSheetIds().findIndex((sheetId) => sheetId === currentSheetId) + 1;
        const formulaId = this.getters.getPivotFormulaId(pivotId);
        this.dispatch("CREATE_SHEET", {
            sheetId,
            name: _t("Pivot #%(formulaId)s", { formulaId }),
            position,
        });
        this.dispatch("ACTIVATE_SHEET", {
            sheetIdFrom: currentSheetId,
            sheetIdTo: sheetId,
        });
        const pivot = this.getters.getPivot(pivotId);
        this.insertPivotWithTable(sheetId, 0, 0, pivotId, pivot.getCollapsedTableStructure().export(), "dynamic");
    }
    duplicatePivotInNewSheet(pivotId, newPivotId, newSheetId) {
        this.dispatch("DUPLICATE_PIVOT", {
            pivotId,
            newPivotId,
            duplicatedPivotName: _t("%s (copy)", this.getters.getPivotCoreDefinition(pivotId).name),
        });
        const activeSheetId = this.getters.getActiveSheetId();
        const position = this.getters.getSheetIds().indexOf(activeSheetId) + 1;
        const formulaId = this.getters.getPivotFormulaId(newPivotId);
        const newPivotName = this.getters.getPivotName(newPivotId);
        const result = this.dispatch("CREATE_SHEET", {
            sheetId: newSheetId,
            name: this.getPivotDuplicateSheetName(_t("%(newPivotName)s (Pivot #%(formulaId)s)", {
                newPivotName,
                formulaId,
            })),
            position,
        });
        if (result.isSuccessful) {
            this.dispatch("ACTIVATE_SHEET", { sheetIdFrom: activeSheetId, sheetIdTo: newSheetId });
            const pivot = this.getters.getPivot(pivotId);
            this.insertPivotWithTable(newSheetId, 0, 0, newPivotId, pivot.getCollapsedTableStructure().export(), "dynamic");
        }
    }
    getPivotDuplicateSheetName(pivotName) {
        const names = this.getters.getSheetIds().map((id) => this.getters.getSheetName(id));
        const sanitizedName = sanitizeSheetName(pivotName);
        return getUniqueText(sanitizedName, names);
    }
    insertPivotWithTable(sheetId, col, row, pivotId, table, mode) {
        const { cols, rows, measures, fieldsType } = table;
        const pivotTable = new SpreadsheetPivotTable(cols, rows, measures, fieldsType || {});
        const numberOfHeaders = pivotTable.columns.length - 1;
        this.resizeSheet(sheetId, col, row, pivotTable);
        const pivotFormulaId = this.getters.getPivotFormulaId(pivotId);
        let zone;
        if (mode === "dynamic") {
            this.dispatch("UPDATE_CELL", {
                sheetId,
                col,
                row,
                content: `=PIVOT(${pivotFormulaId})`,
            });
            zone = {
                left: col,
                right: col,
                top: row,
                bottom: row,
            };
        }
        else {
            this.dispatch("INSERT_PIVOT", {
                sheetId,
                col,
                row,
                pivotId,
                table: pivotTable.export(),
            });
            zone = {
                left: col,
                right: col + pivotTable.getNumberOfDataColumns(),
                top: row,
                bottom: row + numberOfHeaders + pivotTable.rows.length,
            };
        }
        this.dispatch("CREATE_TABLE", {
            tableType: mode,
            sheetId,
            ranges: [this.getters.getRangeDataFromZone(sheetId, zone)],
            config: { ...PIVOT_TABLE_CONFIG, numberOfHeaders },
        });
    }
    resizeSheet(sheetId, col, row, table) {
        const colLimit = table.getNumberOfDataColumns() + 1; // +1 for the Top-Left
        const numberCols = this.getters.getNumberCols(sheetId);
        const deltaCol = numberCols - col;
        if (deltaCol < colLimit) {
            this.dispatch("ADD_COLUMNS_ROWS", {
                dimension: "COL",
                base: numberCols - 1,
                sheetId: sheetId,
                sheetName: this.getters.getSheetName(sheetId),
                quantity: colLimit - deltaCol,
                position: "after",
            });
        }
        const rowLimit = table.columns.length + table.rows.length;
        const numberRows = this.getters.getNumberRows(sheetId);
        const deltaRow = numberRows - row;
        if (deltaRow < rowLimit) {
            this.dispatch("ADD_COLUMNS_ROWS", {
                dimension: "ROW",
                base: numberRows - 1,
                sheetId: sheetId,
                sheetName: this.getters.getSheetName(sheetId),
                quantity: rowLimit - deltaRow,
                position: "after",
            });
        }
    }
    splitPivotFormula(sheetId, col, row, pivotId) {
        const spreadZone = this.getters.getSpreadZone({ sheetId, col, row });
        if (!spreadZone) {
            return;
        }
        const formulaId = this.getters.getPivotFormulaId(pivotId);
        const pivotCells = new Map();
        for (let col = spreadZone.left; col <= spreadZone.right; col++) {
            for (let row = spreadZone.top; row <= spreadZone.bottom; row++) {
                const position = { sheetId, col, row };
                pivotCells.set(position, this.getters.getPivotCellFromPosition(position));
            }
        }
        for (const [position, pivotCell] of pivotCells) {
            this.dispatch("UPDATE_CELL", {
                ...position,
                content: createPivotFormula(formulaId, pivotCell),
            });
        }
        const table = this.getters.getCoreTable({ sheetId, col, row });
        if (table?.type === "dynamic") {
            const zone = positionToZone({ col, row });
            const rangeData = this.getters.getRangeDataFromZone(sheetId, spreadZone);
            this.dispatch("UPDATE_TABLE", {
                sheetId,
                zone,
                newTableRange: rangeData,
                tableType: "static",
            });
        }
    }
}

class SortPlugin extends UIPlugin {
    allowDispatch(cmd) {
        switch (cmd.type) {
            case "SORT_CELLS":
                if (!isInside(cmd.col, cmd.row, cmd.zone)) {
                    return "InvalidSortAnchor" /* CommandResult.InvalidSortAnchor */;
                }
                return this.checkValidations(cmd, this.checkMerge, this.checkMergeSizes, this.checkArrayFormulaInSortZone);
        }
        return "Success" /* CommandResult.Success */;
    }
    handle(cmd) {
        switch (cmd.type) {
            case "SORT_CELLS":
                this.sortZone(cmd.sheetId, cmd, cmd.zone, cmd.sortDirection, cmd.sortOptions || {});
                break;
        }
    }
    checkMerge({ sheetId, zone }) {
        if (!this.getters.doesIntersectMerge(sheetId, zone)) {
            return "Success" /* CommandResult.Success */;
        }
        /*Test the presence of single cells*/
        const singleCells = positions(zone).some(({ col, row }) => !this.getters.isInMerge({ sheetId, col, row }));
        if (singleCells) {
            return "InvalidSortZone" /* CommandResult.InvalidSortZone */;
        }
        return "Success" /* CommandResult.Success */;
    }
    checkMergeSizes({ sheetId, zone }) {
        if (!this.getters.doesIntersectMerge(sheetId, zone)) {
            return "Success" /* CommandResult.Success */;
        }
        const merges = this.getters.getMerges(sheetId).filter((merge) => overlap(merge, zone));
        /*Test the presence of merges of different sizes*/
        const mergeDimension = zoneToDimension(merges[0]);
        const [widthFirst, heightFirst] = [mergeDimension.numberOfCols, mergeDimension.numberOfRows];
        if (!merges.every((merge) => {
            const [widthCurrent, heightCurrent] = [
                merge.right - merge.left + 1,
                merge.bottom - merge.top + 1,
            ];
            return widthCurrent === widthFirst && heightCurrent === heightFirst;
        })) {
            return "InvalidSortZone" /* CommandResult.InvalidSortZone */;
        }
        return "Success" /* CommandResult.Success */;
    }
    checkArrayFormulaInSortZone({ sheetId, zone }) {
        const arrayFormulaInZone = positions(zone).some(({ col, row }) => {
            const originPosition = this.getters.getArrayFormulaSpreadingOn({ sheetId, col, row });
            return originPosition && !deepEquals(originPosition, { sheetId, col, row });
        });
        return arrayFormulaInZone ? "SortZoneWithArrayFormulas" /* CommandResult.SortZoneWithArrayFormulas */ : "Success" /* CommandResult.Success */;
    }
    /**
     * This function evaluates if the top row of a provided zone can be considered as a `header`
     * by checking the following criteria:
     * * If the left-most column top row value (topLeft) is empty, we ignore it while evaluating the criteria.
     * 1 - Apart from the left-most column, every element of the top row must be non-empty, i.e. a cell should be present in the sheet.
     * 2 - There should be at least one column in which the type (CellValueType) of the rop row cell differs from the type of the cell below.
     *  For the second criteria, we ignore columns on which the cell below is empty.
     *
     */
    hasHeader(sheetId, items) {
        if (items[0].length === 1)
            return false;
        let cells = items.map((col) => col.map(({ col, row }) => this.getters.getEvaluatedCell({ sheetId, col, row }).type));
        // ignore left-most column when topLeft cell is empty
        const topLeft = cells[0][0];
        if (topLeft === CellValueType.empty) {
            cells = cells.slice(1);
        }
        if (cells.some((item) => item[0] === CellValueType.empty)) {
            return false;
        }
        else if (cells.some((item) => item[1] !== CellValueType.empty && item[0] !== item[1])) {
            return true;
        }
        else {
            return false;
        }
    }
    sortZone(sheetId, anchor, zone, sortDirection, options) {
        const [stepX, stepY] = this.mainCellsSteps(sheetId, zone);
        const sortingCol = this.getters.getMainCellPosition({
            sheetId,
            col: anchor.col,
            row: anchor.row,
        }).col; // fetch anchor
        const sortZone = Object.assign({}, zone);
        // Update in case of merges in the zone
        let cellPositions = this.mainCells(sheetId, zone);
        if (!options.sortHeaders && this.hasHeader(sheetId, cellPositions)) {
            sortZone.top += stepY;
        }
        cellPositions = this.mainCells(sheetId, sortZone);
        const sortingCells = cellPositions[sortingCol - sortZone.left];
        const sortedIndexOfSortTypeCells = sortCells(sortingCells.map((position) => this.getters.getEvaluatedCell(position)), sortDirection, Boolean(options.emptyCellAsZero));
        const sortedIndex = sortedIndexOfSortTypeCells.map((x) => x.index);
        const [width, height] = [cellPositions.length, cellPositions[0].length];
        const updateCellCommands = [];
        for (let c = 0; c < width; c++) {
            for (let r = 0; r < height; r++) {
                const { col, row, sheetId } = cellPositions[c][sortedIndex[r]];
                const cell = this.getters.getCell({ sheetId, col, row });
                const newCol = sortZone.left + c * stepX;
                const newRow = sortZone.top + r * stepY;
                const newCellValues = {
                    sheetId: sheetId,
                    col: newCol,
                    row: newRow,
                    content: "",
                };
                if (cell) {
                    let content = cell.content;
                    if (cell.isFormula) {
                        const position = this.getters.getCellPosition(cell.id);
                        // we only have a vertical offset
                        content = this.getters.getTranslatedCellFormula(sheetId, 0, newRow - position.row, cell.compiledFormula.tokens);
                    }
                    newCellValues.style = cell.style;
                    newCellValues.content = content;
                    newCellValues.format = cell.format;
                }
                updateCellCommands.push(newCellValues);
            }
        }
        updateCellCommands.forEach((cmdPayload) => this.dispatch("UPDATE_CELL", cmdPayload));
    }
    /**
     * Return the distances between main merge cells in the zone.
     * (1 if there are no merges).
     * Note: it is assumed all merges are the same in the zone.
     */
    mainCellsSteps(sheetId, zone) {
        const merge = this.getters.getMerge({ sheetId, col: zone.left, row: zone.top });
        const stepX = merge ? merge.right - merge.left + 1 : 1;
        const stepY = merge ? merge.bottom - merge.top + 1 : 1;
        return [stepX, stepY];
    }
    /**
     * Return a 2D array of cells in the zone (main merge cells if there are merges)
     */
    mainCells(sheetId, zone) {
        const [stepX, stepY] = this.mainCellsSteps(sheetId, zone);
        const cells = [];
        const cols = range(zone.left, zone.right + 1, stepX);
        const rows = range(zone.top, zone.bottom + 1, stepY);
        for (const col of cols) {
            const colCells = [];
            cells.push(colCells);
            for (const row of rows) {
                colCells.push({ sheetId, col, row });
            }
        }
        return cells;
    }
}

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

class SheetUIPlugin extends UIPlugin {
    static getters = [
        "getCellWidth",
        "getTextWidth",
        "getCellText",
        "getCellMultiLineText",
        "getContiguousZone",
        "computeTextYCoordinate",
    ];
    ctx = document.createElement("canvas").getContext("2d");
    // ---------------------------------------------------------------------------
    // Command Handling
    // ---------------------------------------------------------------------------
    allowDispatch(cmd) {
        return this.chainValidations(this.checkSheetExists, this.checkZonesAreInSheet)(cmd);
    }
    handle(cmd) {
        switch (cmd.type) {
            case "AUTORESIZE_COLUMNS":
                for (const 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.autoResizeRows(cmd.sheetId, cmd.rows);
                break;
            case "DELETE_UNFILTERED_CONTENT":
                const newTarget = [];
                for (const target of cmd.target) {
                    const nonFilteredRows = range(target.top, target.bottom + 1).filter((row) => !this.getters.isRowFiltered(cmd.sheetId, row));
                    const consecutiveRows = groupConsecutive(nonFilteredRows);
                    for (const group of consecutiveRows) {
                        newTarget.push({ ...target, top: group[0], bottom: group[group.length - 1] });
                    }
                }
                this.dispatch("DELETE_CONTENT", { sheetId: cmd.sheetId, target: newTarget });
                break;
        }
    }
    // ---------------------------------------------------------------------------
    // Getters
    // ---------------------------------------------------------------------------
    getCellWidth(position) {
        const style = this.getters.getCellComputedStyle(position);
        let contentWidth = 0;
        const content = this.getters.getEvaluatedCell(position).formattedValue;
        if (content) {
            const multiLineText = splitTextToWidth(this.ctx, content, style, undefined);
            contentWidth += Math.max(...multiLineText.map((line) => computeTextWidth(this.ctx, line, style)));
        }
        for (const icon of this.getters.getCellIcons(position)) {
            contentWidth += icon.margin + icon.size;
        }
        if (this.getters.getDataValidationChipStyle(position)) {
            contentWidth += DATA_VALIDATION_CHIP_MARGIN * 2;
        }
        if (contentWidth === 0) {
            return 0;
        }
        contentWidth += 2 * PADDING_AUTORESIZE_HORIZONTAL;
        if (style.wrapping === "wrap") {
            const colWidth = this.getters.getColSize(this.getters.getActiveSheetId(), position.col);
            return Math.min(colWidth, contentWidth);
        }
        return contentWidth;
    }
    getTextWidth(text, style) {
        return computeTextWidth(this.ctx, text, style);
    }
    getCellText(position, args) {
        const cell = this.getters.getCell(position);
        const locale = this.getters.getLocale();
        if (args?.showFormula && cell?.isFormula) {
            return localizeFormula(cell.content, locale);
        }
        else if (args?.showFormula && !cell?.content) {
            return "";
        }
        else {
            const evaluatedCell = this.getters.getEvaluatedCell(position);
            const formatWidth = args?.availableWidth
                ? {
                    availableWidth: args.availableWidth,
                    measureText: (text) => computeTextWidth(this.ctx, text, cell?.style || {}),
                }
                : undefined;
            return formatValue(evaluatedCell.value, {
                format: evaluatedCell.format,
                locale,
                formatWidth,
            });
        }
    }
    /**
     * Return the text of a cell, split in multiple lines if needed. The text will be split in multiple
     * line if it contains NEWLINE characters, or if it's longer than the given width.
     */
    getCellMultiLineText(position, args) {
        const style = this.getters.getCellStyle(position);
        const text = this.getters.getCellText(position, {
            showFormula: this.getters.shouldShowFormulas(),
            availableWidth: args.maxWidth,
        });
        return splitTextToWidth(this.ctx, text, style, args.wrapText ? args.maxWidth : undefined);
    }
    /** Computes the vertical start point from which a text line should be draw in a cell.
     *
     * Note that in case the cell does not have enough spaces to display its text lines,
     * (wrapping cell case) then the vertical align should be at the top.
     * */
    computeTextYCoordinate(cellRect, textLineHeight, verticalAlign = DEFAULT_VERTICAL_ALIGN, numberOfLines = 1) {
        const y = cellRect.y + 1; // +1 to skip the cell grid line at the top
        const textHeight = computeTextLinesHeight(textLineHeight, numberOfLines);
        const hasEnoughSpaces = cellRect.height > textHeight + MIN_CELL_TEXT_MARGIN * 2;
        if (hasEnoughSpaces) {
            if (verticalAlign === "middle") {
                return Math.ceil(y + (cellRect.height - textHeight) / 2);
            }
            if (verticalAlign === "bottom") {
                return y + cellRect.height - textHeight - MIN_CELL_TEXT_MARGIN;
            }
        }
        return y + MIN_CELL_TEXT_MARGIN;
    }
    /**
     * Expands the given zone until bordered by empty cells or reached the sheet boundaries.
     */
    getContiguousZone(sheetId, zoneToExpand) {
        /** Try to expand the zone by one col/row in any direction to include a new non-empty cell */
        const expandZone = (zone) => {
            for (const col of range(zone.left, zone.right + 1)) {
                if (!this.isCellEmpty({ sheetId, col, row: zone.top - 1 })) {
                    return { ...zone, top: zone.top - 1 };
                }
                if (!this.isCellEmpty({ sheetId, col, row: zone.bottom + 1 })) {
                    return { ...zone, bottom: zone.bottom + 1 };
                }
            }
            for (const row of range(zone.top, zone.bottom + 1)) {
                if (!this.isCellEmpty({ sheetId, col: zone.left - 1, row })) {
                    return { ...zone, left: zone.left - 1 };
                }
                if (!this.isCellEmpty({ sheetId, col: zone.right + 1, row })) {
                    return { ...zone, right: zone.right + 1 };
                }
            }
            return zone;
        };
        let hasExpanded = false;
        let zone = zoneToExpand;
        do {
            hasExpanded = false;
            const newZone = expandZone(zone);
            if (!isEqual(zone, newZone)) {
                hasExpanded = true;
                zone = newZone;
            }
        } while (hasExpanded);
        return zone;
    }
    /**
     * Checks if a cell is empty (i.e. does not have a content or a formula does not spread over it).
     * If the cell is part of a merge, the check applies to the main cell of the merge.
     */
    isCellEmpty(position) {
        const mainPosition = this.getters.getMainCellPosition(position);
        return this.getters.getEvaluatedCell(mainPosition).type === CellValueType.empty;
    }
    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));
    }
    /**
     * Check that any "sheetId" in the command matches an existing
     * sheet.
     */
    checkSheetExists(cmd) {
        if ("sheetId" in cmd && this.getters.tryGetSheet(cmd.sheetId) === undefined) {
            return "InvalidSheetId" /* CommandResult.InvalidSheetId */;
        }
        return "Success" /* CommandResult.Success */;
    }
    /**
     * Check if zones in the command are well formed and
     * not outside the sheet.
     */
    checkZonesAreInSheet(cmd) {
        const sheetId = "sheetId" in cmd ? cmd.sheetId : this.getters.tryGetActiveSheetId();
        if ("ranges" in cmd &&
            cmd.ranges.some((rangeData) => !this.getters.tryGetSheet(rangeData._sheetId))) {
            return "InvalidSheetId" /* CommandResult.InvalidSheetId */;
        }
        const zones = this.getters.getCommandZones(cmd);
        if (!sheetId && zones.length > 0) {
            return "NoActiveSheet" /* CommandResult.NoActiveSheet */;
        }
        if (sheetId && zones.length > 0) {
            return this.getters.checkZonesExistInSheet(sheetId, zones);
        }
        return "Success" /* CommandResult.Success */;
    }
    autoResizeRows(sheetId, rows) {
        const rowSizes = [];
        for (const row of rows) {
            let evaluatedRowSize = 0;
            for (const cellId of this.getters.getRowCells(sheetId, row)) {
                const cell = this.getters.getCellById(cellId);
                if (!cell) {
                    continue;
                }
                const position = this.getters.getCellPosition(cell.id);
                const colSize = this.getters.getColSize(sheetId, position.col);
                if (cell.isFormula || this.getters.getArrayFormulaSpreadingOn(position)) {
                    const content = this.getters.getEvaluatedCell(position).formattedValue;
                    const evaluatedSize = getCellContentHeight(this.ctx, content, cell?.style, colSize);
                    if (evaluatedSize > evaluatedRowSize && evaluatedSize > DEFAULT_CELL_HEIGHT) {
                        evaluatedRowSize = evaluatedSize;
                    }
                }
                else {
                    const content = cell.content;
                    const dynamicRowSize = getCellContentHeight(this.ctx, content, cell?.style, colSize);
                    // Only keep the size of evaluated cells if it's bigger than the dynamic row size
                    if (dynamicRowSize >= evaluatedRowSize && dynamicRowSize > DEFAULT_CELL_HEIGHT) {
                        evaluatedRowSize = 0;
                    }
                }
            }
            rowSizes.push(evaluatedRowSize || null);
        }
        const groupedSizes = new Map(rowSizes.map((size) => [size, []]));
        for (let i = 0; i < rowSizes.length; i++) {
            groupedSizes.get(rowSizes[i])?.push(rows[i]);
        }
        for (const [size, rows] of groupedSizes) {
            this.dispatch("RESIZE_COLUMNS_ROWS", {
                elements: rows,
                dimension: "ROW",
                size,
                sheetId,
            });
        }
    }
}

class TableComputedStylePlugin extends UIPlugin {
    static getters = ["getCellTableStyle", "getCellTableBorder"];
    tableStyles = {};
    handle(cmd) {
        if (invalidateEvaluationCommands.has(cmd.type) ||
            (cmd.type === "UPDATE_CELL" && ("content" in cmd || "format" in cmd)) ||
            cmd.type === "EVALUATE_CELLS") {
            this.tableStyles = {};
            return;
        }
        if (doesCommandInvalidatesTableStyle(cmd)) {
            if ("sheetId" in cmd) {
                delete this.tableStyles[cmd.sheetId];
            }
            else {
                this.tableStyles = {};
            }
            return;
        }
    }
    finalize() {
        for (const sheetId of this.getters.getSheetIds()) {
            if (!this.tableStyles[sheetId]) {
                this.tableStyles[sheetId] = {};
            }
            for (const table of this.getters.getTables(sheetId)) {
                if (!this.tableStyles[sheetId][table.id]) {
                    this.tableStyles[sheetId][table.id] = this.computeTableStyle(sheetId, table);
                }
            }
        }
    }
    getCellTableStyle(position) {
        const table = this.getters.getTable(position);
        if (!table) {
            return undefined;
        }
        return this.tableStyles[position.sheetId][table.id]().styles[position.col]?.[position.row];
    }
    getCellTableBorder(position) {
        const table = this.getters.getTable(position);
        if (!table) {
            return undefined;
        }
        return this.tableStyles[position.sheetId][table.id]().borders[position.col]?.[position.row];
    }
    computeTableStyle(sheetId, table) {
        return lazy(() => {
            const { config, numberOfCols, numberOfRows } = this.getTableRuntimeConfig(sheetId, table);
            const style = this.getters.getTableStyle(table.config.styleId);
            const relativeTableStyle = getComputedTableStyle(config, style, numberOfCols, numberOfRows);
            // Return the style with sheet coordinates instead of tables coordinates
            const mapping = this.getTableMapping(sheetId, table);
            const absoluteTableStyle = { borders: {}, styles: {} };
            for (let col = 0; col < numberOfCols; col++) {
                const colInSheet = mapping.colMapping[col];
                absoluteTableStyle.borders[colInSheet] = {};
                absoluteTableStyle.styles[colInSheet] = {};
                for (let row = 0; row < numberOfRows; row++) {
                    const rowInSheet = mapping.rowMapping[row];
                    absoluteTableStyle.borders[colInSheet][rowInSheet] = relativeTableStyle.borders[col][row];
                    absoluteTableStyle.styles[colInSheet][rowInSheet] = relativeTableStyle.styles[col][row];
                }
            }
            return absoluteTableStyle;
        });
    }
    /**
     * Get the actual table config that will be used to compute the table style. It is different from
     * the config of the table because of hidden rows and columns in the sheet. For example remove the
     * hidden rows from config.numberOfHeaders.
     */
    getTableRuntimeConfig(sheetId, table) {
        const tableZone = table.range.zone;
        const config = { ...table.config };
        let numberOfCols = tableZone.right - tableZone.left + 1;
        let numberOfRows = tableZone.bottom - tableZone.top + 1;
        for (let row = tableZone.top; row <= tableZone.bottom; row++) {
            if (!this.getters.isRowHidden(sheetId, row)) {
                continue;
            }
            numberOfRows--;
            if (row - tableZone.top < table.config.numberOfHeaders) {
                config.numberOfHeaders--;
                if (config.numberOfHeaders < 0) {
                    config.numberOfHeaders = 0;
                }
            }
            if (row === tableZone.bottom) {
                config.totalRow = false;
            }
        }
        for (let col = tableZone.left; col <= tableZone.right; col++) {
            if (!this.getters.isColHidden(sheetId, col)) {
                continue;
            }
            numberOfCols--;
            if (col === tableZone.left) {
                config.firstColumn = false;
            }
            if (col === tableZone.right) {
                config.lastColumn = false;
            }
        }
        return {
            config,
            numberOfCols,
            numberOfRows,
        };
    }
    /**
     * Get a mapping: relative col/row position in the table <=> col/row in the sheet
     */
    getTableMapping(sheetId, table) {
        const colMapping = {};
        const rowMapping = {};
        let colOffset = 0;
        let rowOffset = 0;
        const tableZone = table.range.zone;
        for (let col = tableZone.left; col <= tableZone.right; col++) {
            if (this.getters.isColHidden(sheetId, col)) {
                continue;
            }
            colMapping[colOffset] = col;
            colOffset++;
            for (let row = tableZone.top; row <= tableZone.bottom; row++) {
                if (this.getters.isRowHidden(sheetId, row)) {
                    continue;
                }
                rowMapping[rowOffset] = row;
                rowOffset++;
            }
        }
        return {
            colMapping,
            rowMapping,
        };
    }
}
const invalidateTableStyleCommands = [
    "HIDE_COLUMNS_ROWS",
    "UNHIDE_COLUMNS_ROWS",
    "UNFOLD_HEADER_GROUP",
    "UNGROUP_HEADERS",
    "FOLD_HEADER_GROUP",
    "FOLD_ALL_HEADER_GROUPS",
    "UNFOLD_ALL_HEADER_GROUPS",
    "FOLD_HEADER_GROUPS_IN_ZONE",
    "UNFOLD_HEADER_GROUPS_IN_ZONE",
    "CREATE_TABLE",
    "UPDATE_TABLE",
    "UPDATE_FILTER",
    "REMOVE_TABLE",
    "RESIZE_TABLE",
    "CREATE_TABLE_STYLE",
    "REMOVE_TABLE_STYLE",
];
const invalidateTableStyleCommandsSet = new Set(invalidateTableStyleCommands);
function doesCommandInvalidatesTableStyle(cmd) {
    return invalidateTableStyleCommandsSet.has(cmd.type);
}

class CellComputedStylePlugin extends UIPlugin {
    static getters = ["getCellComputedBorder", "getCellComputedStyle"];
    styles = new PositionMap();
    borders = new PositionMap();
    handle(cmd) {
        if (invalidateEvaluationCommands.has(cmd.type) ||
            cmd.type === "UPDATE_CELL" ||
            cmd.type === "SET_FORMATTING" ||
            cmd.type === "ADD_DATA_VALIDATION_RULE" ||
            cmd.type === "REMOVE_DATA_VALIDATION_RULE" ||
            cmd.type === "EVALUATE_CELLS") {
            this.styles = new PositionMap();
            this.borders = new PositionMap();
            return;
        }
        if (doesCommandInvalidatesTableStyle(cmd)) {
            if ("sheetId" in cmd) {
                this.styles.clearSheet(cmd.sheetId);
                this.borders.clearSheet(cmd.sheetId);
            }
            else {
                this.styles = new PositionMap();
                this.borders = new PositionMap();
            }
            return;
        }
        if (invalidateCFEvaluationCommands.has(cmd.type)) {
            this.styles = new PositionMap();
            return;
        }
        if (invalidateBordersCommands.has(cmd.type)) {
            this.borders = new PositionMap();
            return;
        }
    }
    getCellComputedBorder(position) {
        let border = this.borders.get(position);
        if (border === undefined) {
            border = this.computeCellBorder(position);
            this.borders.set(position, border);
        }
        return border;
    }
    getCellComputedStyle(position) {
        let style = this.styles.get(position);
        if (style === undefined) {
            style = this.computeCellStyle(position);
            this.styles.set(position, style);
        }
        return style;
    }
    computeCellBorder(position) {
        const cellBorder = this.getters.getCellBorder(position) || {};
        const cellTableBorder = this.getters.getCellTableBorder(position) || {};
        // Use removeFalsyAttributes to avoid overwriting borders with undefined values
        const border = {
            ...removeFalsyAttributes(cellTableBorder),
            ...removeFalsyAttributes(cellBorder),
        };
        return isObjectEmptyRecursive(border) ? null : border;
    }
    computeCellStyle(position) {
        const cell = this.getters.getCell(position);
        const cfStyle = this.getters.getCellConditionalFormatStyle(position);
        const tableStyle = this.getters.getCellTableStyle(position);
        const dataValidationStyle = this.getters.getDataValidationCellStyle(position);
        const computedStyle = {
            ...removeFalsyAttributes(tableStyle),
            ...removeFalsyAttributes(dataValidationStyle),
            ...removeFalsyAttributes(cell?.style),
            ...removeFalsyAttributes(cfStyle),
        };
        const evaluatedCell = this.getters.getEvaluatedCell(position);
        if (evaluatedCell.link && !computedStyle.textColor) {
            computedStyle.textColor = LINK_COLOR;
        }
        return computedStyle;
    }
}

class CheckboxTogglePlugin extends UIPlugin {
    static getters = ["hasBooleanValidationInZones"];
    handle(cmd) {
        switch (cmd.type) {
            case "TOGGLE_CHECKBOX":
                this.toggleCheckbox(cmd.sheetId, cmd.target);
                break;
        }
    }
    hasBooleanValidationInZones(zones) {
        const sheetId = this.getters.getActiveSheetId();
        for (const zone of zones) {
            for (let col = zone.left; col <= zone.right; col++) {
                for (let row = zone.top; row <= zone.bottom; row++) {
                    if (this.getters.isCellValidCheckbox({ col, row, sheetId })) {
                        return true;
                    }
                }
            }
        }
        return false;
    }
    toggleCheckbox(sheetId, target) {
        for (const zone of target) {
            for (let col = zone.left; col <= zone.right; col++) {
                for (let row = zone.top; row <= zone.bottom; row++) {
                    const position = { col, row, sheetId };
                    if (this.getters.isCellValidCheckbox(position)) {
                        const content = this.getters.getEvaluatedCell(position).value ? "FALSE" : "TRUE";
                        this.dispatch("UPDATE_CELL", {
                            ...position,
                            content,
                        });
                    }
                }
            }
        }
    }
}

class DataValidationInsertionPlugin extends UIPlugin {
    handle(cmd) {
        switch (cmd.type) {
            case "ADD_DATA_VALIDATION_RULE":
                if (cmd.rule.criterion.type === "isBoolean") {
                    const ranges = cmd.ranges.map((range) => this.getters.getRangeFromRangeData(range));
                    for (const position of getCellPositionsInRanges(ranges)) {
                        const cell = this.getters.getCell(position);
                        const evaluatedCell = this.getters.getEvaluatedCell(position);
                        if (!cell?.content) {
                            this.dispatch("UPDATE_CELL", { ...position, content: "FALSE" });
                            // In this case, a cell has been updated in the core plugin but
                            // not yet evaluated. This can occur after a paste operation.
                        }
                        else if (cell?.content && evaluatedCell.type === CellValueType.empty) {
                            let value;
                            if (cell.content.startsWith("=")) {
                                const result = this.getters.evaluateFormula(position.sheetId, cell.content);
                                value = (isMatrix(result) ? result[0][0] : result)?.toString();
                            }
                            else {
                                value = cell.content;
                            }
                            if (!value || !isBoolean(value)) {
                                this.dispatch("UPDATE_CELL", { ...position, content: "FALSE" });
                            }
                        }
                        else if (evaluatedCell.type !== CellValueType.boolean) {
                            this.dispatch("UPDATE_CELL", { ...position, content: "FALSE" });
                        }
                    }
                }
        }
    }
}

/**
 * This plugin provides dynamic translation getter. In o-spreadsheet, it has
 * no implementation, but this plugin can be replaced by another one to provide
 * a real implementation.
 *
 * For example, in Odoo, the plugin is replaced by a plugin that used the
 * module namespace to dynamically translate terms.
 */
class DynamicTranslate extends UIPlugin {
    static getters = ["dynamicTranslate"];
    dynamicTranslate(term) {
        return term;
    }
}

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

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

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

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

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

class PivotPresenceTracker {
    trackedValues = new Set();
    domainToArray(domain) {
        return domain.flatMap((node) => [node.field, toString(node.value)]);
    }
    isValuePresent(measure, domain) {
        const key = JSON.stringify({ measure, domain: this.domainToArray(domain) });
        return this.trackedValues.has(key);
    }
    isHeaderPresent(domain) {
        const key = JSON.stringify({ domain: this.domainToArray(domain) });
        return this.trackedValues.has(key);
    }
    trackValue(measure, domain) {
        const key = JSON.stringify({ measure, domain: this.domainToArray(domain) });
        this.trackedValues.add(key);
    }
    trackHeader(domain) {
        const key = JSON.stringify({ domain: this.domainToArray(domain) });
        this.trackedValues.add(key);
    }
}

class PivotPresencePlugin extends UIPlugin {
    static getters = ["getPivotPresenceTracker"];
    trackPresencePivotId;
    tracker;
    handle(cmd) {
        switch (cmd.type) {
            case "PIVOT_START_PRESENCE_TRACKING":
                this.tracker = new PivotPresenceTracker();
                this.trackPresencePivotId = cmd.pivotId;
                break;
            case "PIVOT_STOP_PRESENCE_TRACKING":
                this.trackPresencePivotId = undefined;
                break;
        }
    }
    getPivotPresenceTracker(pivotId) {
        if (this.trackPresencePivotId !== pivotId) {
            return undefined;
        }
        if (!this.tracker) {
            throw new Error("Tracker not initialized");
        }
        return this.tracker;
    }
}

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

class TableAutofillPlugin extends UIPlugin {
    handle(cmd) {
        switch (cmd.type) {
            case "AUTOFILL_TABLE_COLUMN":
                const table = this.getters.getCoreTable(cmd);
                const cell = this.getters.getCell(cmd);
                if (!table?.config.automaticAutofill || table.type === "dynamic" || !cell?.isFormula) {
                    return;
                }
                const { col, row } = cmd;
                const tableContentZone = getTableContentZone(table.range.zone, table.config);
                if (tableContentZone && isInside(col, row, tableContentZone)) {
                    const top = cmd.autofillRowStart ?? tableContentZone.top;
                    const bottom = cmd.autofillRowEnd ?? tableContentZone.bottom;
                    const autofillZone = { ...tableContentZone, top, bottom };
                    this.autofillTableZone(cmd, autofillZone);
                }
                break;
        }
    }
    autofillTableZone(autofillSource, zone) {
        if (zone.top === zone.bottom) {
            return;
        }
        const { col, row, sheetId } = autofillSource;
        for (let r = zone.top; r <= zone.bottom; r++) {
            if (r === row) {
                continue;
            }
            if (this.getters.getEvaluatedCell({ col, row: r, sheetId }).type !== CellValueType.empty) {
                return;
            }
        }
        // TODO: seems odd that autofill a table column have side effects on the selection. Autofill commands should be
        // refactored to take the selection as a parameter
        const oldSelection = {
            zone: this.getters.getSelectedZone(),
            cell: this.getters.getActivePosition(),
        };
        this.selection.selectCell(col, row);
        this.dispatch("AUTOFILL_SELECT", { col, row: zone.bottom });
        this.dispatch("AUTOFILL");
        this.selection.selectCell(col, row);
        this.dispatch("AUTOFILL_SELECT", { col, row: zone.top });
        this.dispatch("AUTOFILL");
        this.selection.selectZone(oldSelection);
    }
}

class TableResizeUI extends UIPlugin {
    allowDispatch(cmd) {
        switch (cmd.type) {
            case "RESIZE_TABLE":
                const table = this.getters.getCoreTableMatchingTopLeft(cmd.sheetId, cmd.zone);
                if (!table) {
                    return "TableNotFound" /* CommandResult.TableNotFound */;
                }
                const oldTableZone = table.range.zone;
                const newTableZone = this.getters.getRangeFromRangeData(cmd.newTableRange).zone;
                if (newTableZone.top !== oldTableZone.top || newTableZone.left !== oldTableZone.left) {
                    return "InvalidTableResize" /* CommandResult.InvalidTableResize */;
                }
                return this.canDispatch("UPDATE_TABLE", { ...cmd }).reasons;
        }
        return "Success" /* CommandResult.Success */;
    }
    handle(cmd) {
        switch (cmd.type) {
            case "RESIZE_TABLE": {
                const table = this.getters.getCoreTableMatchingTopLeft(cmd.sheetId, cmd.zone);
                this.dispatch("UPDATE_TABLE", { ...cmd });
                if (!table)
                    return;
                const newTableZone = this.getters.getRangeFromRangeData(cmd.newTableRange).zone;
                this.selection.selectCell(newTableZone.right, newTableZone.bottom);
                if (!table.config.automaticAutofill)
                    return;
                const oldTableZone = table.range.zone;
                if (newTableZone.bottom >= oldTableZone.bottom) {
                    for (let col = newTableZone.left; col <= newTableZone.right; col++) {
                        const autofillSource = { col, row: oldTableZone.bottom, sheetId: cmd.sheetId };
                        if (this.getters.getCell(autofillSource)?.content.startsWith("=")) {
                            this.dispatch("AUTOFILL_TABLE_COLUMN", {
                                ...autofillSource,
                                autofillRowStart: oldTableZone.bottom,
                                autofillRowEnd: newTableZone.bottom,
                            });
                        }
                    }
                    break;
                }
            }
        }
    }
}

const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5 MB
/**
 * Clipboard Plugin
 *
 * This clipboard manages all cut/copy/paste interactions internal to the
 * application, and with the OS clipboard as well.
 */
class ClipboardPlugin extends UIPlugin {
    static layers = ["Clipboard"];
    static getters = [
        "getClipboardTextAndImageContent",
        "getClipboardId",
        "getClipboardTextContent",
        "isCutOperation",
    ];
    status = "invisible";
    originSheetId;
    copiedData;
    _isCutOperation = false;
    clipboardId = new UuidGenerator().uuidv4();
    fileStore;
    uuidGenerator;
    constructor(config) {
        super(config);
        this.fileStore = config.external.fileStore;
        this.uuidGenerator = new UuidGenerator();
    }
    // ---------------------------------------------------------------------------
    // Command Handling
    // ---------------------------------------------------------------------------
    allowDispatch(cmd) {
        switch (cmd.type) {
            case "CUT":
                const zones = this.getters.getSelectedZones();
                return this.isCutAllowedOn(zones);
            case "PASTE_FROM_OS_CLIPBOARD": {
                const copiedData = this.convertTextToClipboardData(cmd.clipboardContent.text ?? "");
                const pasteOption = cmd.pasteOption;
                return this.isPasteAllowed(cmd.target, copiedData, { pasteOption, isCutOperation: false });
            }
            case "PASTE": {
                if (!this.copiedData) {
                    return "EmptyClipboard" /* CommandResult.EmptyClipboard */;
                }
                const pasteOption = cmd.pasteOption;
                return this.isPasteAllowed(cmd.target, this.copiedData, {
                    pasteOption: pasteOption,
                    isCutOperation: this._isCutOperation,
                });
            }
            case "COPY_PASTE_CELLS_ABOVE": {
                const zones = this.getters.getSelectedZones();
                if (zones.length > 1 || (zones[0].top === 0 && zones[0].bottom === 0)) {
                    return "InvalidCopyPasteSelection" /* CommandResult.InvalidCopyPasteSelection */;
                }
                break;
            }
            case "COPY_PASTE_CELLS_ON_LEFT": {
                const zones = this.getters.getSelectedZones();
                if (zones.length > 1 || (zones[0].left === 0 && zones[0].right === 0)) {
                    return "InvalidCopyPasteSelection" /* CommandResult.InvalidCopyPasteSelection */;
                }
                break;
            }
            case "INSERT_CELL": {
                const { cut, paste } = this.getInsertCellsTargets(cmd.zone, cmd.shiftDimension);
                const copiedData = this.copy(cut, "shiftCells");
                return this.isPasteAllowed(paste, copiedData, { isCutOperation: true });
            }
            case "DELETE_CELL": {
                const { cut, paste } = this.getDeleteCellsTargets(cmd.zone, cmd.shiftDimension);
                const copiedData = this.copy(cut, "shiftCells");
                return this.isPasteAllowed(paste, copiedData, { isCutOperation: true });
            }
        }
        return "Success" /* CommandResult.Success */;
    }
    handle(cmd) {
        switch (cmd.type) {
            case "COPY":
            case "CUT":
                const zones = this.getters.getSelectedZones();
                this.status = "visible";
                this.originSheetId = this.getters.getActiveSheetId();
                this._isCutOperation = cmd.type === "CUT";
                this.copiedData = this.copy(zones);
                break;
            case "PASTE_FROM_OS_CLIPBOARD": {
                this._isCutOperation = false;
                const htmlData = cmd.clipboardContent.data;
                // TODO: support multiple image import
                if (cmd.clipboardContent.imageData) {
                    const sheetId = this.getters.getActiveSheetId();
                    const figureId = this.uuidGenerator.uuidv4();
                    const definition = cmd.clipboardContent.imageData;
                    const size = getMaxFigureSize(this.getters, definition.size);
                    this.dispatch("CREATE_IMAGE", {
                        definition,
                        size,
                        col: cmd.target[0].left,
                        row: cmd.target[0].top,
                        offset: { x: 0, y: 0 },
                        sheetId,
                        figureId,
                    });
                }
                if (htmlData) {
                    this.copiedData = htmlData;
                }
                else {
                    this.copiedData = this.convertTextToClipboardData(cmd.clipboardContent.text ?? "");
                }
                const pasteOption = cmd.pasteOption;
                this.paste(cmd.target, this.copiedData, {
                    pasteOption,
                    selectTarget: true,
                    isCutOperation: false,
                });
                this.status = "invisible";
                this.copiedData = undefined;
                break;
            }
            case "PASTE": {
                const pasteOption = cmd.pasteOption;
                this.paste(cmd.target, this.copiedData, {
                    pasteOption,
                    selectTarget: true,
                    isCutOperation: this._isCutOperation,
                });
                this.status = "invisible";
                if (this._isCutOperation) {
                    this.copiedData = undefined;
                    this._isCutOperation = false;
                }
                break;
            }
            case "COPY_PASTE_CELLS_ABOVE":
                {
                    const zone = this.getters.getSelectedZone();
                    const multipleRowsInSelection = zone.top !== zone.bottom;
                    const copyTarget = {
                        ...zone,
                        bottom: multipleRowsInSelection ? zone.top : zone.top - 1,
                        top: multipleRowsInSelection ? zone.top : zone.top - 1,
                    };
                    this.originSheetId = this.getters.getActiveSheetId();
                    const copiedData = this.copy([copyTarget]);
                    this.paste([zone], copiedData, {
                        isCutOperation: false,
                        selectTarget: true,
                    });
                }
                break;
            case "COPY_PASTE_CELLS_ON_LEFT":
                {
                    const zone = this.getters.getSelectedZone();
                    const multipleColsInSelection = zone.left !== zone.right;
                    const copyTarget = {
                        ...zone,
                        right: multipleColsInSelection ? zone.left : zone.left - 1,
                        left: multipleColsInSelection ? zone.left : zone.left - 1,
                    };
                    this.originSheetId = this.getters.getActiveSheetId();
                    const copiedData = this.copy([copyTarget]);
                    this.paste([zone], copiedData, {
                        isCutOperation: false,
                        selectTarget: true,
                    });
                }
                break;
            case "CLEAN_CLIPBOARD_HIGHLIGHT":
                this.status = "invisible";
                break;
            case "DELETE_CELL": {
                const { cut, paste } = this.getDeleteCellsTargets(cmd.zone, cmd.shiftDimension);
                if (!isZoneValid(cut[0])) {
                    this.dispatch("CLEAR_CELLS", {
                        target: [cmd.zone],
                        sheetId: this.getters.getActiveSheetId(),
                    });
                    break;
                }
                const copiedData = this.copy(cut, "shiftCells");
                this.paste(paste, copiedData, { isCutOperation: true });
                break;
            }
            case "INSERT_CELL": {
                const { cut, paste } = this.getInsertCellsTargets(cmd.zone, cmd.shiftDimension);
                const copiedData = this.copy(cut, "shiftCells");
                this.paste(paste, copiedData, { isCutOperation: true });
                break;
            }
            case "ADD_COLUMNS_ROWS": {
                this.status = "invisible";
                // If we add a col/row inside or before the cut area, we invalidate the clipboard
                if (!this._isCutOperation || cmd.sheetId !== this.copiedData?.sheetId) {
                    return;
                }
                const isClipboardDirty = this.isColRowDirtyingClipboard(cmd.position === "before" ? cmd.base : cmd.base + 1, cmd.dimension);
                if (isClipboardDirty) {
                    this.copiedData = undefined;
                }
                break;
            }
            case "REMOVE_COLUMNS_ROWS": {
                this.status = "invisible";
                // If we remove a col/row inside or before the cut area, we invalidate the clipboard
                if (!this._isCutOperation || cmd.sheetId !== this.copiedData?.sheetId) {
                    return;
                }
                for (const el of cmd.elements) {
                    const isClipboardDirty = this.isColRowDirtyingClipboard(el, cmd.dimension);
                    if (isClipboardDirty) {
                        this.copiedData = undefined;
                        break;
                    }
                }
                this.status = "invisible";
                break;
            }
            case "REPEAT_PASTE": {
                this.paste(cmd.target, this.copiedData, {
                    isCutOperation: false,
                    pasteOption: cmd.pasteOption,
                    selectTarget: true,
                });
                break;
            }
            case "DELETE_SHEET":
                if (!this._isCutOperation) {
                    return;
                }
                if (this.originSheetId === cmd.sheetId) {
                    this.copiedData = undefined;
                    this.status = "invisible";
                }
                break;
            default:
                if (isCoreCommand(cmd)) {
                    this.status = "invisible";
                }
        }
    }
    convertTextToClipboardData(clipboardData) {
        const handlers = this.selectClipboardHandlers({ figureId: true }).concat(this.selectClipboardHandlers({}));
        const copiedData = {};
        for (const { handlerName, handler } of handlers) {
            const data = handler.convertTextToClipboardData(clipboardData);
            copiedData[handlerName] = data;
            const minimalKeys = ["sheetId", "cells", "zones", "figureId"];
            for (const key of minimalKeys) {
                if (data && key in data) {
                    copiedData[key] = data[key];
                }
            }
        }
        return copiedData;
    }
    selectClipboardHandlers(data) {
        const handlersRegistry = "figureId" in data
            ? clipboardHandlersRegistries.figureHandlers
            : clipboardHandlersRegistries.cellHandlers;
        return handlersRegistry.getKeys().map((handlerName) => {
            const Handler = handlersRegistry.get(handlerName);
            return { handlerName, handler: new Handler(this.getters, this.dispatch) };
        });
    }
    isCutAllowedOn(zones) {
        const clipboardData = this.getClipboardData(zones);
        for (const { handler } of this.selectClipboardHandlers(clipboardData)) {
            const result = handler.isCutAllowed(clipboardData);
            if (result !== "Success" /* CommandResult.Success */) {
                return result;
            }
        }
        return "Success" /* CommandResult.Success */;
    }
    isPasteAllowed(target, copiedData, options) {
        for (const { handler } of this.selectClipboardHandlers(copiedData)) {
            const result = handler.isPasteAllowed(this.getters.getActiveSheetId(), target, copiedData, {
                ...options,
            });
            if (result !== "Success" /* CommandResult.Success */) {
                return result;
            }
        }
        return "Success" /* CommandResult.Success */;
    }
    isColRowDirtyingClipboard(position, dimension) {
        if (!this.copiedData || !this.copiedData.zones) {
            return false;
        }
        const { zones } = this.copiedData;
        for (const zone of zones) {
            if (dimension === "COL" && position <= zone.right) {
                return true;
            }
            if (dimension === "ROW" && position <= zone.bottom) {
                return true;
            }
        }
        return false;
    }
    copy(zones, mode = "copyPaste") {
        const copiedData = {};
        const clipboardData = this.getClipboardData(zones);
        for (const { handlerName, handler } of this.selectClipboardHandlers(clipboardData)) {
            const data = handler.copy(clipboardData, this._isCutOperation, mode);
            copiedData[handlerName] = data;
            const minimalKeys = ["sheetId", "cells", "zones", "figureId"];
            for (const key of minimalKeys) {
                if (data && key in data) {
                    copiedData[key] = data[key];
                }
            }
        }
        return copiedData;
    }
    paste(zones, copiedData, options) {
        if (!copiedData) {
            return;
        }
        const sheetId = this.getters.getActiveSheetId();
        const handlers = this.selectClipboardHandlers(copiedData);
        const { target, zone, selectedZones } = getPasteTargetFromHandlers(sheetId, zones, copiedData, handlers, options);
        if (zone !== undefined) {
            this.addMissingDimensions(sheetId, zone.right - zone.left + 1, zone.bottom - zone.top + 1, zone.left, zone.top);
        }
        applyClipboardHandlersPaste(handlers, copiedData, target, options);
        if (!options?.selectTarget) {
            return;
        }
        selectPastedZone(this.selection, zones, selectedZones);
    }
    /**
     * Add columns and/or rows to ensure that col + width and row + height are still
     * in the sheet
     */
    addMissingDimensions(sheetId, width, height, col, row) {
        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,
                sheetName: this.getters.getSheetName(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,
                sheetName: this.getters.getSheetName(sheetId),
                quantity: missingCols,
                position: "after",
            });
        }
    }
    // ---------------------------------------------------------------------------
    // 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.
     */
    getClipboardTextContent() {
        return this.getPlainTextContent();
    }
    getClipboardId() {
        return this.clipboardId;
    }
    async getClipboardTextAndImageContent() {
        const file = await this.getImageContent();
        const mime = file?.type;
        const content = {
            [ClipboardMIMEType.PlainText]: this.getPlainTextContent(),
            [ClipboardMIMEType.Html]: await this.getHTMLContent(),
        };
        if (mime && file) {
            content[mime] = file;
        }
        return content;
    }
    getSheetData() {
        const data = {
            version: getCurrentVersion(),
            clipboardId: this.clipboardId,
        };
        if (this.copiedData && "figureId" in this.copiedData) {
            return data;
        }
        return {
            ...data,
            ...this.copiedData,
        };
    }
    getPlainTextContent() {
        if (!this.copiedData?.cells) {
            return "\t";
        }
        return (this.copiedData.cells
            .map((cells) => {
            return cells
                .map((c) => this.getters.shouldShowFormulas() && c?.tokens?.length
                ? c?.content || ""
                : c.evaluatedCell?.formattedValue || "")
                .join("\t");
        })
            .join("\n") || "\t");
    }
    async getHTMLContent() {
        let innerHTML = "";
        const cells = this.copiedData?.cells;
        if (!cells) {
            if (this.copiedData?.figureId) {
                const figureId = this.copiedData.figureId;
                const figureSheetId = this.getters.getFigureSheetId(figureId);
                const figure = this.getters.getFigure(figureSheetId, figureId);
                if (figure.tag === "image") {
                    innerHTML = await this.craftImageHTML(figureId);
                }
                else {
                    innerHTML = "\t";
                }
            }
            else {
                innerHTML = "\t";
            }
        }
        else if (cells.length === 1 && cells[0].length === 1) {
            innerHTML = `${this.getters.getCellText(cells[0][0].position)}`;
        }
        else if (!cells[0][0]) {
            return "";
        }
        else {
            let htmlTable = `<table border="1" style="border-collapse:collapse">`;
            for (const row of cells) {
                htmlTable += "<tr>";
                for (const cell of row) {
                    if (!cell) {
                        continue;
                    }
                    const cssStyle = cssPropertiesToCss(cellStyleToCss(this.getters.getCellComputedStyle(cell.position)));
                    const cellText = this.getters.getCellText(cell.position);
                    htmlTable += `<td style="${cssStyle}">` + xmlEscape(cellText) + "</td>";
                }
                htmlTable += "</tr>";
            }
            htmlTable += "</table>";
            innerHTML = htmlTable;
        }
        const serializedData = JSON.stringify(this.getSheetData());
        return `<div data-osheet-clipboard='${xmlEscape(serializedData)}'>${innerHTML}</div>`;
    }
    readFileAsDataURL(blob) {
        return new Promise((resolve) => {
            const reader = new FileReader();
            reader.onload = () => resolve(reader.result);
            reader.readAsDataURL(blob);
        });
    }
    async craftImageHTML(figureId) {
        if (!this.fileStore) {
            return "\t";
        }
        const imageUrl = this.getters.getImage(figureId).path;
        const file = (await this.fileStore?.getFile(imageUrl)) || null;
        if (file) {
            const imageUrl = (await this.readFileAsDataURL(file));
            return `<img src="${xmlEscape(imageUrl)}" />`;
        }
        else {
            return "\t";
        }
    }
    async getImageContent() {
        const figureId = this.copiedData?.figureId;
        if (!figureId) {
            return;
        }
        const figureSheetId = this.getters.getFigureSheetId(figureId);
        const figure = this.getters.getFigure(figureSheetId, figureId);
        let file;
        if (figure.tag === "image") {
            if (!this.fileStore) {
                return;
            }
            const imageUrl = this.getters.getImage(figureId).path;
            file = await this.fileStore?.getFile(imageUrl);
            // we can only write on image/png format in the clipboard
            // So we convert the image to png if it's not already
            if (file.type !== "image/png") {
                if (file.size > MAX_FILE_SIZE) {
                    this.ui.notifyUI({
                        text: _t("The file you are trying to copy is too large (>%sMB).\nIt will not be added to your OS clipboard.\nYou can download it directly instead.", Math.round(MAX_FILE_SIZE / (1024 * 1024))),
                        sticky: false,
                        type: "warning",
                    });
                    return undefined;
                }
                file = await convertImageToPng(imageUrl);
            }
        }
        if (!file) {
            return undefined;
        }
        else {
            return file instanceof File ? file : new File([file], "image.png", { type: "image/png" });
        }
    }
    isCutOperation() {
        return this._isCutOperation ?? false;
    }
    // ---------------------------------------------------------------------------
    // 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] };
    }
    getClipboardData(zones) {
        const sheetId = this.getters.getActiveSheetId();
        const selectedFigureId = this.getters.getSelectedFigureId();
        if (selectedFigureId) {
            return { figureId: selectedFigureId, sheetId };
        }
        const data = getClipboardDataPositions(sheetId, zones);
        if (!this._isCutOperation) {
            data.rowsIndexes = data.rowsIndexes.filter((r) => !this.getters.isRowFiltered(sheetId, r));
        }
        return data;
    }
    // ---------------------------------------------------------------------------
    // Grid rendering
    // ---------------------------------------------------------------------------
    drawLayer(renderingContext) {
        if (this.status !== "visible" || !this.copiedData) {
            return;
        }
        const { sheetId, zones } = this.copiedData;
        if (sheetId !== this.getters.getActiveSheetId() || !zones || !zones.length) {
            return;
        }
        const { ctx, thinLineWidth } = renderingContext;
        ctx.setLineDash([8, 5]);
        ctx.strokeStyle = SELECTION_BORDER_COLOR;
        ctx.lineWidth = 3.3 * thinLineWidth;
        for (const zone of zones) {
            const { x, y, width, height } = this.getters.getVisibleRect(zone);
            if (width > 0 && height > 0) {
                ctx.strokeRect(x, y, width, height);
            }
        }
    }
}

const EMPTY_CRITERION = { filterType: "criterion", type: "none", values: [] };
class FilterEvaluationPlugin extends UIPlugin {
    static getters = [
        "getFilterValue",
        "getFilterHiddenValues",
        "getFilterCriterionValue",
        "getFirstTableInSelection",
        "isRowFiltered",
        "isFilterActive",
    ];
    filterValues = {};
    hiddenRows = {};
    isEvaluationDirty = false;
    allowDispatch(cmd) {
        switch (cmd.type) {
            case "UPDATE_FILTER":
                if (!this.getters.getFilterId(cmd)) {
                    return "FilterNotFound" /* CommandResult.FilterNotFound */;
                }
                break;
        }
        return "Success" /* CommandResult.Success */;
    }
    handle(cmd) {
        switch (cmd.type) {
            case "UNDO":
            case "REDO":
            case "UPDATE_CELL":
            case "EVALUATE_CELLS":
            case "ACTIVATE_SHEET":
            case "REMOVE_TABLE":
            case "ADD_COLUMNS_ROWS":
            case "REMOVE_COLUMNS_ROWS":
            case "UPDATE_TABLE":
                this.isEvaluationDirty = true;
                break;
            case "START":
                for (const sheetId of this.getters.getSheetIds()) {
                    this.filterValues[sheetId] = {};
                }
                break;
            case "CREATE_SHEET":
                this.filterValues[cmd.sheetId] = {};
                break;
            case "HIDE_COLUMNS_ROWS":
            case "UNHIDE_COLUMNS_ROWS":
            case "GROUP_HEADERS":
            case "UNGROUP_HEADERS":
            case "FOLD_HEADER_GROUP":
            case "UNFOLD_HEADER_GROUP":
            case "FOLD_ALL_HEADER_GROUPS":
            case "UNFOLD_ALL_HEADER_GROUPS":
            case "FOLD_HEADER_GROUPS_IN_ZONE":
            case "UNFOLD_HEADER_GROUPS_IN_ZONE":
                this.updateHiddenRows(cmd.sheetId);
                break;
            case "UPDATE_FILTER":
                this.updateFilter(cmd);
                this.updateHiddenRows(cmd.sheetId);
                break;
            case "DUPLICATE_SHEET":
                this.filterValues[cmd.sheetIdTo] = deepCopy(this.filterValues[cmd.sheetId]);
                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) {
            for (const sheetId of this.getters.getSheetIds()) {
                this.updateHiddenRows(sheetId);
            }
            this.isEvaluationDirty = false;
        }
    }
    isRowFiltered(sheetId, row) {
        return !!this.hiddenRows[sheetId]?.has(row);
    }
    getFilterValue(position) {
        const id = this.getters.getFilterId(position);
        const sheetId = position.sheetId;
        return id ? this.filterValues[sheetId]?.[id] : undefined;
    }
    getFilterHiddenValues(position) {
        const id = this.getters.getFilterId(position);
        const sheetId = position.sheetId;
        if (!id || !this.filterValues[sheetId])
            return [];
        const value = this.filterValues[sheetId][id] || [];
        return value.filterType === "values" ? value.hiddenValues : [];
    }
    getFilterCriterionValue(position) {
        const id = this.getters.getFilterId(position);
        const sheetId = position.sheetId;
        if (!id || !this.filterValues[sheetId])
            return EMPTY_CRITERION;
        const value = this.filterValues[sheetId][id];
        return value && value.filterType === "criterion" ? value : EMPTY_CRITERION;
    }
    isFilterActive(position) {
        const id = this.getters.getFilterId(position);
        if (!id)
            return false;
        const sheetId = position.sheetId;
        const value = this.filterValues[sheetId]?.[id];
        if (!value)
            return false;
        return value.filterType === "values" ? value.hiddenValues.length > 0 : value.type !== "none";
    }
    getFirstTableInSelection() {
        const sheetId = this.getters.getActiveSheetId();
        const selection = this.getters.getSelectedZones();
        return this.getters.getTablesOverlappingZones(sheetId, selection)[0];
    }
    updateFilter({ col, row, value, sheetId }) {
        const id = this.getters.getFilterId({ sheetId, col, row });
        if (!id)
            return;
        if (!this.filterValues[sheetId])
            this.filterValues[sheetId] = {};
        this.filterValues[sheetId][id] = value;
    }
    updateHiddenRows(sheetId) {
        const filters = this.getters
            .getFilters(sheetId)
            .sort((filter1, filter2) => filter1.rangeWithHeaders.zone.top - filter2.rangeWithHeaders.zone.top);
        const hiddenRows = new Set();
        for (const filter of filters) {
            // Disable filters whose header are hidden
            const filterValue = this.filterValues[sheetId]?.[filter.id];
            const filteredZone = filter.filteredRange?.zone;
            if (!filterValue ||
                !filteredZone ||
                hiddenRows.has(filter.rangeWithHeaders.zone.top) ||
                this.getters.isRowHiddenByUser(sheetId, filter.rangeWithHeaders.zone.top)) {
                continue;
            }
            if (filterValue.filterType === "values") {
                const filteredValues = filterValue.hiddenValues?.map(toLowerCase);
                if (!filteredValues)
                    continue;
                const filteredValuesSet = new Set(filteredValues);
                for (let row = filteredZone.top; row <= filteredZone.bottom; row++) {
                    const value = this.getCellValueAsString(sheetId, filter.col, row);
                    if (filteredValuesSet.has(value)) {
                        hiddenRows.add(row);
                    }
                }
            }
            else {
                if (filterValue.type === "none")
                    continue;
                const evaluator = criterionEvaluatorRegistry.get(filterValue.type);
                const evaluatedCriterionValues = filterValue.values.map((value) => {
                    if (!value.startsWith("=")) {
                        return parseLiteral(value, DEFAULT_LOCALE);
                    }
                    return this.getters.evaluateFormula(sheetId, value) ?? "";
                });
                if (evaluatedCriterionValues.some(isMultipleElementMatrix)) {
                    continue;
                }
                const evaluatedCriterion = {
                    type: filterValue.type,
                    values: evaluatedCriterionValues.map(toScalar),
                    dateValue: filterValue.dateValue,
                };
                for (let row = filteredZone.top; row <= filteredZone.bottom; row++) {
                    const position = { sheetId, col: filter.col, row };
                    const value = this.getters.getEvaluatedCell(position).value ?? "";
                    if (!evaluator.isValueValid(value, evaluatedCriterion, this.getters, sheetId)) {
                        hiddenRows.add(row);
                    }
                }
            }
        }
        this.hiddenRows[sheetId] = hiddenRows;
    }
    getCellValueAsString(sheetId, col, row) {
        const value = this.getters.getEvaluatedCell({ sheetId, col, row }).formattedValue;
        return value.toLowerCase();
    }
    exportForExcel(data) {
        for (const sheetData of data.sheets) {
            const sheetId = sheetData.id;
            for (const tableData of sheetData.tables) {
                const tableZone = toZone(tableData.range);
                const filters = [];
                const headerNames = [];
                for (const i of range(0, zoneToDimension(tableZone).numberOfCols)) {
                    const position = {
                        sheetId: sheetData.id,
                        col: tableZone.left + i,
                        row: tableZone.top,
                    };
                    const filteredValues = this.getFilterHiddenValues(position);
                    const filter = this.getters.getFilter(position);
                    const valuesInFilterZone = filter?.filteredRange
                        ? positions(filter.filteredRange.zone).map((position) => this.getters.getEvaluatedCell({ sheetId, ...position }).formattedValue)
                        : [];
                    if (filteredValues.length) {
                        const xlsxDisplayedValues = valuesInFilterZone
                            .filter((val) => val)
                            .filter((val) => !filteredValues.includes(val));
                        filters.push({
                            colId: i,
                            displayedValues: [...new Set(xlsxDisplayedValues)],
                            displayBlanks: !filteredValues.includes("") && valuesInFilterZone.some((val) => !val),
                        });
                    }
                    // In xlsx, column header should ALWAYS be a string and should be unique in the table
                    const headerString = this.getters.getEvaluatedCell(position).formattedValue;
                    const headerName = this.getUniqueColNameForExcel(i, headerString, headerNames);
                    headerNames.push(headerName);
                    const xc = toXC(position.col, position.row);
                    sheetData.cells[xc] = headerName;
                    sheetData.cellValues[xc] = headerName;
                }
                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}`;
        }
        return getUniqueText(colName, usedColNames, {
            compute: (name, i) => colName + String(i),
            start: 2,
        });
    }
}

/**
 * SelectionPlugin
 */
class GridSelectionPlugin extends UIPlugin {
    static layers = ["Selection"];
    static getters = [
        "getActiveSheet",
        "getActiveSheetId",
        "getActiveSheetName",
        "getActiveCell",
        "getActiveCols",
        "getActiveRows",
        "getCurrentStyle",
        "getSelectedZones",
        "getSelectedZone",
        "getSelectedCells",
        "getSelectedFigureId",
        "getSelection",
        "getActivePosition",
        "getSheetPosition",
        "isSingleColSelected",
        "getElementsFromSelection",
        "tryGetActiveSheetId",
        "isGridSelectionActive",
        "getSelectecUnboundedZone",
    ];
    gridSelection = {
        anchor: {
            cell: { col: 0, row: 0 },
            zone: { top: 0, left: 0, bottom: 0, right: 0 },
        },
        zones: [{ top: 0, left: 0, bottom: 0, right: 0 }],
    };
    selectedFigureId = null;
    sheetsData = {};
    moveClient;
    isUnbounded;
    // This flag is used to avoid to historize the ACTIVE_SHEET command when it's
    // the main command.
    activeSheet = null;
    constructor(config) {
        super(config);
        this.moveClient = config.moveClient;
        this.isUnbounded = false;
    }
    // ---------------------------------------------------------------------------
    // Command Handling
    // ---------------------------------------------------------------------------
    allowDispatch(cmd) {
        switch (cmd.type) {
            case "ACTIVATE_SHEET":
                try {
                    const sheet = this.getters.getSheet(cmd.sheetIdTo);
                    if (!sheet.isVisible) {
                        return "SheetIsHidden" /* CommandResult.SheetIsHidden */;
                    }
                    break;
                }
                catch (error) {
                    return "InvalidSheetId" /* CommandResult.InvalidSheetId */;
                }
            case "MOVE_COLUMNS_ROWS":
                return this.isMoveElementAllowed(cmd);
        }
        return "Success" /* CommandResult.Success */;
    }
    handleEvent(event) {
        const anchor = event.anchor;
        let zones = [];
        this.isUnbounded = event.options?.unbounded || false;
        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 "ACTIVATE_SHEET":
                this.selectedFigureId = null;
                break;
            case "DELETE_FIGURE":
                if (this.selectedFigureId === cmd.figureId) {
                    this.selectedFigureId = null;
                }
                break;
            case "DELETE_SHEET":
                if (this.selectedFigureId && this.getters.getFigure(cmd.sheetId, this.selectedFigureId)) {
                    this.selectedFigureId = null;
                }
                break;
        }
        switch (cmd.type) {
            case "START":
                const firstSheetId = this.getters.getVisibleSheetIds()[0];
                this.activateSheet(firstSheetId, firstSheetId);
                const { col, row } = this.getters.getNextVisibleCellPosition({
                    sheetId: firstSheetId,
                    col: 0,
                    row: 0,
                });
                this.selectCell(col, row);
                this.selection.registerAsDefault(this, this.gridSelection.anchor, {
                    handleEvent: this.handleEvent.bind(this),
                });
                this.moveClient({ sheetId: firstSheetId, col: 0, row: 0 });
                break;
            case "ACTIVATE_SHEET": {
                this.activateSheet(cmd.sheetIdFrom, cmd.sheetIdTo);
                break;
            }
            case "REMOVE_COLUMNS_ROWS": {
                const sheetId = this.getters.getActiveSheetId();
                if (cmd.sheetId === sheetId) {
                    if (cmd.dimension === "COL") {
                        this.onColumnsRemoved(cmd);
                    }
                    else {
                        this.onRowsRemoved(cmd);
                    }
                    const { col, row } = this.gridSelection.anchor.cell;
                    this.moveClient({ sheetId, col, row });
                }
                break;
            }
            case "ADD_COLUMNS_ROWS": {
                const sheetId = this.getters.getActiveSheetId();
                if (cmd.sheetId === sheetId) {
                    this.onAddElements(cmd);
                    const { col, row } = this.gridSelection.anchor.cell;
                    this.moveClient({ sheetId, col, row });
                }
                break;
            }
            case "MOVE_COLUMNS_ROWS":
                if (cmd.sheetId === this.getActiveSheetId()) {
                    this.onMoveElements(cmd);
                }
                break;
            case "SELECT_FIGURE":
                this.selectedFigureId = cmd.figureId;
                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;
    }
    getActiveSheetName() {
        return this.activeSheet.name;
    }
    tryGetActiveSheetId() {
        return this.activeSheet?.id;
    }
    getActiveCell() {
        return this.getters.getEvaluatedCell(this.getActivePosition());
    }
    getActiveCols() {
        const activeCols = new Set();
        for (const 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 (const zone of this.gridSelection.zones) {
            if (zone.left === 0 && zone.right === this.getters.getNumberCols(sheetId) - 1) {
                for (let i = zone.top; i <= zone.bottom; i++) {
                    activeRows.add(i);
                }
            }
        }
        return activeRows;
    }
    getCurrentStyle() {
        const zone = this.getters.getSelectedZone();
        const sheetId = this.getters.getActiveSheetId();
        return this.getters.getCellStyle({ sheetId, col: zone.left, row: zone.top });
    }
    getSelectedZones() {
        return deepCopy(this.gridSelection.zones);
    }
    getSelectedZone() {
        return deepCopy(this.gridSelection.anchor.zone);
    }
    getSelectecUnboundedZone() {
        const zone = this.isUnbounded
            ? this.getters.getUnboundedZone(this.activeSheet.id, this.gridSelection.anchor.zone)
            : this.gridSelection.anchor.zone;
        return deepCopy(zone);
    }
    getSelection() {
        return deepCopy(this.gridSelection);
    }
    getSelectedCells() {
        const sheetId = this.getters.getActiveSheetId();
        const cells = [];
        for (const zone of this.gridSelection.zones) {
            cells.push(...this.getters.getEvaluatedCellsInZone(sheetId, zone));
        }
        return cells;
    }
    getSelectedFigureId() {
        return this.selectedFigureId;
    }
    getActivePosition() {
        return this.getters.getMainCellPosition({
            sheetId: this.getActiveSheetId(),
            col: this.gridSelection.anchor.cell.col,
            row: this.gridSelection.anchor.cell.row,
        });
    }
    getSheetPosition(sheetId) {
        if (sheetId === this.getters.getActiveSheetId()) {
            return this.getActivePosition();
        }
        else {
            const sheetData = this.sheetsData[sheetId];
            return sheetData
                ? {
                    sheetId,
                    col: sheetData.gridSelection.anchor.cell.col,
                    row: sheetData.gridSelection.anchor.cell.row,
                }
                : this.getters.getNextVisibleCellPosition({ sheetId, col: 0, row: 0 });
        }
    }
    isSingleColSelected() {
        const selection = this.getters.getSelectedZones();
        if (selection.length !== 1 || selection[0].left !== selection[0].right) {
            return false;
        }
        return true;
    }
    /**
     * Returns a sorted array of indexes of all columns (respectively rows depending
     * on the dimension parameter) intersected by the currently selected zones.
     *
     * example:
     * assume selectedZones: [{left:0, right: 2, top :2, bottom: 4}, {left:5, right: 6, top :3, bottom: 5}]
     *
     * if dimension === "COL" => [0,1,2,5,6]
     * if dimension === "ROW" => [2,3,4,5]
     */
    getElementsFromSelection(dimension) {
        if (dimension === "COL" && this.getters.getActiveCols().size === 0) {
            return [];
        }
        if (dimension === "ROW" && this.getters.getActiveRows().size === 0) {
            return [];
        }
        const zones = this.getters.getSelectedZones();
        let elements = [];
        const start = dimension === "COL" ? "left" : "top";
        const end = dimension === "COL" ? "right" : "bottom";
        for (const zone of zones) {
            const zoneRows = Array.from({ length: zone[end] - zone[start] + 1 }, (_, i) => zone[start] + i);
            elements = elements.concat(zoneRows);
        }
        return [...new Set(elements)].sort();
    }
    // ---------------------------------------------------------------------------
    // Other
    // ---------------------------------------------------------------------------
    activateSheet(sheetIdFrom, sheetIdTo) {
        this.setActiveSheet(sheetIdTo);
        this.sheetsData[sheetIdFrom] = {
            gridSelection: deepCopy(this.gridSelection),
        };
        if (sheetIdTo in this.sheetsData) {
            Object.assign(this, this.sheetsData[sheetIdTo]);
            this.selection.resetDefaultAnchor(this, deepCopy(this.gridSelection.anchor));
        }
        else {
            const { col, row } = this.getters.getNextVisibleCellPosition({
                sheetId: sheetIdTo,
                col: 0,
                row: 0,
            });
            this.selectCell(col, row);
        }
        const { col, row } = this.gridSelection.anchor.cell;
        this.moveClient({ sheetId: this.activeSheet.id, col, row });
    }
    /**
     * Ensure selections are not outside sheet boundaries.
     * They are clipped to fit inside the sheet if needed.
     */
    setSelectionMixin(anchor, zones) {
        const { anchor: clippedAnchor, zones: clippedZones } = this.clipSelection(this.getters.getActiveSheetId(), { anchor, zones });
        this.gridSelection.anchor = clippedAnchor;
        this.gridSelection.zones = uniqueZones(clippedZones);
    }
    /**
     * Change the anchor of the selection active cell to an absolute col and row index.
     *
     * This is a non trivial task. We need to stop the editing process and update
     * properly the current selection.  Also, this method can optionally create a new
     * range in the selection.
     */
    selectCell(col, row) {
        const sheetId = this.getters.getActiveSheetId();
        const zone = this.getters.expandZone(sheetId, { left: col, right: col, top: row, bottom: row });
        this.setSelectionMixin({ zone, cell: { col, row } }, [zone]);
    }
    setActiveSheet(id) {
        this.activeSheet = this.getters.getSheet(id);
    }
    activateNextSheet(direction) {
        const sheetIds = this.getters.getSheetIds();
        const oldSheetPosition = sheetIds.findIndex((id) => id === this.activeSheet.id);
        const delta = direction === "left" ? sheetIds.length - 1 : 1;
        const newPosition = (oldSheetPosition + delta) % sheetIds.length;
        this.dispatch("ACTIVATE_SHEET", {
            sheetIdFrom: this.getActiveSheetId(),
            sheetIdTo: sheetIds[newPosition],
        });
    }
    onColumnsRemoved(cmd) {
        const { cell, zone } = this.gridSelection.anchor;
        const selectedZone = updateSelectionOnDeletion(zone, "left", [...cmd.elements]);
        let anchorZone = { left: cell.col, right: cell.col, top: cell.row, bottom: cell.row };
        anchorZone = updateSelectionOnDeletion(anchorZone, "left", [...cmd.elements]);
        const anchor = {
            cell: {
                col: anchorZone.left,
                row: anchorZone.top,
            },
            zone: selectedZone,
        };
        const selections = this.gridSelection.zones.map((zone) => updateSelectionOnDeletion(zone, "left", [...cmd.elements]));
        this.setSelectionMixin(anchor, selections);
    }
    onRowsRemoved(cmd) {
        const { cell, zone } = this.gridSelection.anchor;
        const selectedZone = updateSelectionOnDeletion(zone, "top", [...cmd.elements]);
        let anchorZone = { left: cell.col, right: cell.col, top: cell.row, bottom: cell.row };
        anchorZone = updateSelectionOnDeletion(anchorZone, "top", [...cmd.elements]);
        const anchor = {
            cell: {
                col: anchorZone.left,
                row: anchorZone.top,
            },
            zone: selectedZone,
        };
        const selections = this.gridSelection.zones.map((zone) => updateSelectionOnDeletion(zone, "top", [...cmd.elements]));
        this.setSelectionMixin(anchor, selections);
    }
    onAddElements(cmd) {
        const start = cmd.dimension === "COL" ? "left" : "top";
        const anchorZone = updateSelectionOnInsertion(this.gridSelection.anchor.zone, start, cmd.base, cmd.position, cmd.quantity);
        const selection = this.gridSelection.zones.map((zone) => updateSelectionOnInsertion(zone, start, cmd.base, cmd.position, cmd.quantity));
        const anchor = {
            cell: { col: anchorZone.left, row: anchorZone.top },
            zone: anchorZone,
        };
        this.setSelectionMixin(anchor, selection);
    }
    onMoveElements(cmd) {
        const thickness = cmd.elements.length;
        const figureUpdateCmds = this.getFiguresUpdates(cmd);
        this.dispatch("ADD_COLUMNS_ROWS", {
            dimension: cmd.dimension,
            sheetId: cmd.sheetId,
            sheetName: cmd.sheetName,
            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 toRemove = isBasedBefore ? cmd.elements.map((el) => el + thickness) : cmd.elements;
        const originalSize = {};
        for (const element of toRemove) {
            originalSize[element] = this.getters.getHeaderSize(cmd.sheetId, cmd.dimension, element);
        }
        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 sheetId = this.getActiveSheetId();
        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,
            },
        ];
        // The clipboards copy the data before pasting to ensure that
        // clipboardA paste doesn't interfere with clipboardB copy
        const handlers = clipboardHandlersRegistries.cellHandlers.getKeys().map((name) => {
            const Handler = clipboardHandlersRegistries.cellHandlers.get(name);
            return [name, new Handler(this.getters, this.dispatch)];
        });
        const data = {};
        for (const [handlerName, handler] of handlers) {
            data[handlerName] = handler.copy(getClipboardDataPositions(sheetId, target), false, "shiftCells");
        }
        for (const [handlerName, handler] of handlers) {
            if (!data[handlerName]) {
                continue;
            }
            handler.paste({ zones: pasteTarget, sheetId }, data[handlerName], { isCutOperation: true });
        }
        const selection = pasteTarget[0];
        const col = selection.left;
        const row = selection.top;
        this.setSelectionMixin({ zone: selection, cell: { col, row } }, [selection]);
        let currentIndex = isBasedBefore ? cmd.base : cmd.base + 1;
        const resizingGroups = {};
        for (const element of toRemove) {
            const size = originalSize[element];
            const currentSize = this.getters.getHeaderSize(cmd.sheetId, cmd.dimension, currentIndex);
            if (size !== currentSize) {
                resizingGroups[size] ??= [];
                resizingGroups[size].push(currentIndex);
            }
            currentIndex += 1;
        }
        for (const size in resizingGroups) {
            this.dispatch("RESIZE_COLUMNS_ROWS", {
                dimension: cmd.dimension,
                sheetId: cmd.sheetId,
                size: parseInt(size, 10),
                elements: resizingGroups[size],
            });
        }
        this.dispatch("REMOVE_COLUMNS_ROWS", {
            dimension: cmd.dimension,
            sheetId: cmd.sheetId,
            sheetName: cmd.sheetName,
            elements: toRemove,
        });
        this.applyFigureUpdates(figureUpdateCmds);
    }
    getFiguresUpdates(cmd) {
        const cmds = [];
        const moveDict = {};
        const base = cmd.elements[0] < cmd.base ? cmd.base - cmd.elements.length + 1 : cmd.base;
        for (let index = 0; index < cmd.elements.length; index++) {
            moveDict[cmd.elements[index]] = base + index;
        }
        for (const figure of this.getters.getFigures(cmd.sheetId)) {
            if (cmd.dimension === "COL" && figure.col in moveDict) {
                cmds.push({
                    type: "UPDATE_FIGURE",
                    sheetId: cmd.sheetId,
                    figureId: figure.id,
                    col: moveDict[figure.col],
                    row: figure.row,
                });
            }
            if (cmd.dimension === "ROW" && figure.row in moveDict) {
                cmds.push({
                    type: "UPDATE_FIGURE",
                    sheetId: cmd.sheetId,
                    figureId: figure.id,
                    row: moveDict[figure.row],
                    col: figure.col,
                });
            }
        }
        return cmds;
    }
    applyFigureUpdates(cmds) {
        for (const cmd of cmds) {
            this.dispatch(cmd.type, { ...cmd });
        }
    }
    isMoveElementAllowed(cmd) {
        const isCol = cmd.dimension === "COL";
        const start = cmd.elements[0];
        const end = cmd.elements[cmd.elements.length - 1];
        const id = cmd.sheetId;
        const doesElementsHaveCommonMerges = isCol
            ? this.getters.doesColumnsHaveCommonMerges
            : this.getters.doesRowsHaveCommonMerges;
        if (doesElementsHaveCommonMerges(id, start - 1, start) ||
            doesElementsHaveCommonMerges(id, end, end + 1) ||
            doesElementsHaveCommonMerges(id, cmd.base - 1, cmd.base)) {
            return "WillRemoveExistingMerge" /* CommandResult.WillRemoveExistingMerge */;
        }
        const headers = [cmd.base, ...cmd.elements];
        const maxHeaderValue = isCol ? this.getters.getNumberCols(id) : this.getters.getNumberRows(id);
        if (headers.some((h) => h < 0 || h >= maxHeaderValue)) {
            return "InvalidHeaderIndex" /* CommandResult.InvalidHeaderIndex */;
        }
        if (!isCol && !this.isTableRowMoveAllowed(id, cmd.elements)) {
            return "CannotMoveTableHeader" /* CommandResult.CannotMoveTableHeader */;
        }
        return "Success" /* CommandResult.Success */;
    }
    isTableRowMoveAllowed(sheetId, selectedRows) {
        const tables = this.getters.getCoreTables(sheetId);
        if (tables.length === 0) {
            return true;
        }
        const selectedRowSet = new Set(selectedRows);
        return tables.every(({ range: { zone }, config }) => {
            const { top, bottom } = zone;
            if (config.numberOfHeaders === 0) {
                return true;
            }
            const headerRowEnd = top + config.numberOfHeaders - 1;
            // Moving the table is allowed if table header rows are not part of the selection
            // Or if the entire table (including header) is selected
            const isHeaderSelected = selectedRows.some((row) => row >= top && row <= headerRowEnd);
            if (!isHeaderSelected) {
                return true;
            }
            const isWholeTableSelected = range(top, bottom + 1).every((r) => selectedRowSet.has(r));
            return isWholeTableSelected;
        });
    }
    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
    // ---------------------------------------------------------------------------
    drawLayer(renderingContext) {
        if (this.getters.isDashboard()) {
            return;
        }
        const { ctx, thinLineWidth } = renderingContext;
        // selection
        const zones = this.getSelectedZones();
        ctx.fillStyle = "#f3f7fe";
        const onlyOneCell = zones.length === 1 && zones[0].left === zones[0].right && zones[0].top === zones[0].bottom;
        ctx.fillStyle = onlyOneCell ? "#f3f7fe" : "#e9f0ff";
        ctx.strokeStyle = SELECTION_BORDER_COLOR;
        ctx.lineWidth = 1.5 * thinLineWidth;
        for (const zone of zones) {
            const { x, y, width, height } = this.getters.getVisibleRect(zone);
            ctx.globalCompositeOperation = "multiply";
            ctx.fillRect(x, y, width, height);
            ctx.globalCompositeOperation = "source-over";
            ctx.strokeRect(x, y, width, height);
        }
        ctx.globalCompositeOperation = "source-over";
        // active zone
        const position = this.getActivePosition();
        ctx.strokeStyle = SELECTION_BORDER_COLOR;
        ctx.lineWidth = 3 * thinLineWidth;
        let zone;
        if (this.getters.isInMerge(position)) {
            zone = this.getters.getMerge(position);
        }
        else {
            zone = positionToZone(position);
        }
        const { x, y, width, height } = this.getters.getVisibleRect(zone);
        if (width > 0 && height > 0) {
            ctx.strokeRect(x, y, width, height);
        }
    }
}

class InternalViewport {
    getters;
    sheetId;
    boundaries;
    top;
    bottom;
    left;
    right;
    offsetX;
    offsetY;
    canScrollVertically;
    canScrollHorizontally;
    viewportWidth;
    viewportHeight;
    offsetCorrectionX;
    offsetCorrectionY;
    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.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 } = this.getters.getColDimensions(this.sheetId, lastCol);
        const { end: lastRowEnd } = this.getters.getRowDimensions(this.sheetId, lastRow);
        let width = lastColEnd - this.offsetCorrectionX;
        if (this.canScrollHorizontally) {
            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(height, this.viewportHeight); // if the viewport grid size is smaller than its client height, return client height
            if (lastRowEnd + FOOTER_HEIGHT > height && !this.getters.isReadonly()) {
                height += FOOTER_HEIGHT;
            }
        }
        return { width, height };
    }
    /**
     * Return the index of a column given an offset x, based on the pane left
     * visible cell.
     * It returns -1 if no column is found.
     */
    getColIndex(x) {
        if (x < this.offsetCorrectionX || x > this.offsetCorrectionX + this.viewportWidth) {
            return -1;
        }
        return this.searchHeaderIndex("COL", x - this.offsetCorrectionX + this.snapCorrection.x, this.left);
    }
    /**
     * Return the index of a row given an offset y, based on the pane top
     * visible cell.
     * It returns -1 if no row is found.
     */
    getRowIndex(y) {
        if (y < this.offsetCorrectionY || y > this.offsetCorrectionY + this.viewportHeight) {
            return -1;
        }
        return this.searchHeaderIndex("ROW", y - this.offsetCorrectionY + this.snapCorrection.y, this.top);
    }
    /**
     * This function will make sure that the provided cell position (or current selected position) is part of
     * the pane that is actually displayed on the client. We therefore adjust the offset of the pane
     * until it contains the cell completely.
     */
    adjustPosition(position) {
        const sheetId = this.sheetId;
        const mainCellPosition = this.getters.getMainCellPosition({ sheetId, ...position });
        const { col, row } = this.getters.getNextVisibleCellPosition(mainCellPosition);
        if (isInside(col, this.boundaries.top, this.boundaries)) {
            this.adjustPositionX(col);
        }
        if (isInside(this.boundaries.left, row, this.boundaries)) {
            this.adjustPositionY(row);
        }
    }
    adjustPositionX(targetCol) {
        const sheetId = this.sheetId;
        const { start, end } = this.getters.getColDimensions(sheetId, targetCol);
        if (this.offsetX + this.viewportWidth + this.offsetCorrectionX < end) {
            this.offsetX = end - this.viewportWidth;
        }
        else if (this.offsetX + this.offsetCorrectionX > start) {
            this.offsetX = start - this.offsetCorrectionX;
        }
        this.adjustViewportZoneX();
    }
    adjustPositionY(targetRow) {
        const sheetId = this.sheetId;
        const { start, end } = this.getters.getRowDimensions(sheetId, targetRow);
        if (this.offsetY + this.viewportHeight + this.offsetCorrectionY < end) {
            this.offsetY = end - this.viewportHeight;
        }
        else if (this.offsetY + this.offsetCorrectionY > start) {
            this.offsetY = start - this.offsetCorrectionY;
        }
        this.adjustViewportZoneY();
    }
    willNewOffsetScrollViewport(offsetX, offsetY) {
        return ((this.canScrollHorizontally && this.offsetX !== offsetX) ||
            (this.canScrollVertically && this.offsetY !== offsetY));
    }
    setViewportOffset(offsetX, offsetY) {
        this.setViewportOffsetX(offsetX);
        this.setViewportOffsetY(offsetY);
    }
    adjustViewportZone() {
        this.adjustViewportZoneX();
        this.adjustViewportZoneY();
    }
    /**
     *
     * Computes the visible coordinates & dimensions of a given zone inside the viewport
     *
     */
    getVisibleRect(zone) {
        const targetZone = intersection(zone, this);
        const scrollDeltaX = this.snapCorrection.x;
        const scrollDeltaY = this.snapCorrection.y;
        if (targetZone) {
            const x = this.getters.getColRowOffset("COL", this.left, targetZone.left) +
                this.offsetCorrectionX -
                (this.left !== targetZone.left ? scrollDeltaX : 0);
            const y = this.getters.getColRowOffset("ROW", this.top, targetZone.top) +
                this.offsetCorrectionY -
                (this.top !== targetZone.top ? scrollDeltaY : 0);
            const width = Math.min(this.getters.getColRowOffset("COL", targetZone.left, targetZone.right + 1) -
                (this.left === targetZone.left ? scrollDeltaX : 0), this.viewportWidth);
            const height = Math.min(this.getters.getColRowOffset("ROW", targetZone.top, targetZone.bottom + 1) -
                (this.top === targetZone.top ? scrollDeltaY : 0), this.viewportHeight);
            return { x, y, width, height };
        }
        return undefined;
    }
    /**
     *
     * @returns Computes the absolute coordinates & dimensions of a given zone inside the viewport
     *
     */
    getFullRect(zone) {
        const targetZone = intersection(zone, this.boundaries);
        const scrollDeltaX = this.snapCorrection.x;
        const scrollDeltaY = this.snapCorrection.y;
        if (targetZone) {
            const x = this.getters.getColRowOffset("COL", this.left, zone.left) + this.offsetCorrectionX;
            const y = this.getters.getColRowOffset("ROW", this.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: x - scrollDeltaX, y: y - scrollDeltaY, width, height };
        }
        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));
    }
    searchHeaderIndex(dimension, position, startIndex = 0) {
        if (this.viewportWidth <= 0 || this.viewportHeight <= 0) {
            return -1;
        }
        const sheetId = this.sheetId;
        const headers = this.getters.getNumberHeaders(sheetId, dimension);
        // using a binary search:
        let start = startIndex;
        let end = headers;
        while (start <= end && start !== headers && end !== -1) {
            const mid = Math.floor((start + end) / 2);
            const offset = this.getters.getColRowOffset(dimension, startIndex, mid);
            const size = this.getters.getHeaderSize(sheetId, dimension, mid);
            if (position >= offset && position < offset + size) {
                return mid;
            }
            else if (position >= offset + size) {
                start = mid + 1;
            }
            else {
                end = mid - 1;
            }
        }
        return -1;
    }
    setViewportOffsetX(offsetX) {
        if (!this.canScrollHorizontally) {
            return;
        }
        this.offsetX = offsetX;
        this.adjustViewportZoneX();
    }
    setViewportOffsetY(offsetY) {
        if (!this.canScrollVertically) {
            return;
        }
        this.offsetY = 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.offsetX > viewportWidth) {
                this.offsetX = Math.max(0, viewportWidth - this.viewportWidth);
            }
        }
        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.offsetY > paneHeight) {
                this.offsetY = Math.max(0, paneHeight - this.viewportHeight);
            }
        }
        this.adjustViewportZoneY();
    }
    /** Updates the pane zone and snapped offset based on its horizontal
     * offset (will find Left) and its width (will find Right) */
    adjustViewportZoneX() {
        this.left = this.searchHeaderIndex("COL", this.offsetX, this.boundaries.left);
        this.right = Math.min(this.boundaries.right, this.searchHeaderIndex("COL", 
        // if we hit the border of two cells, we want to match the previous
        Math.max(this.viewportWidth + this.snapCorrection.x - 0.1), this.left));
        if (!this.viewportWidth) {
            return;
        }
        if (this.left === -1) {
            this.left = this.boundaries.left;
        }
        if (this.right === -1) {
            this.right = this.boundaries.right;
        }
    }
    /** Updates the pane zone and snapped offset based on its vertical
     * offset (will find Top) and its width (will find Bottom) */
    adjustViewportZoneY() {
        this.top = this.searchHeaderIndex("ROW", this.offsetY, this.boundaries.top);
        this.bottom = Math.min(this.boundaries.bottom, this.searchHeaderIndex("ROW", 
        // if we hit the border of two cells, we want to match the previous
        Math.max(this.viewportHeight + this.snapCorrection.y - 0.1, 0), this.top));
        if (!this.viewportHeight) {
            return;
        }
        if (this.top === -1) {
            this.top = this.boundaries.top;
        }
        if (this.bottom === -1) {
            this.bottom = this.boundaries.bottom;
        }
    }
    /** represents the part of the header on the topLeft that could be partially
     * hidden due to the scroll
     * */
    get snapCorrection() {
        return {
            x: Math.abs(this.offsetX -
                this.getters.getColRowOffset("COL", this.boundaries.left, Math.max(0, this.left))),
            y: Math.abs(this.offsetY -
                this.getters.getColRowOffset("ROW", this.boundaries.top, Math.max(0, this.top))),
        };
    }
}

/**
 *   EdgeScrollCases Schema
 *
 *  The dots/double dots represent a freeze (= a split of viewports)
 *  In this example, we froze vertically between columns D and E
 *  and horizontally between rows 4 and 5.
 *
 *  One can see that we scrolled horizontally from column E to G and
 *  vertically from row 5 to 7.
 *
 *     A  B  C  D   G  H  I  J  K  L  M  N  O  P  Q  R  S  T
 *     _______________________________________________________
 *  1 |           :                                           |
 *  2 |           :                                           |
 *  3 |           :        B   ↑                 6            |
 *  4 |           :        |   |                 |            |
 *     ····················+···+·················+············|
 *  7 |           :        |   |                 |            |
 *  8 |           :        ↓   2                 |            |
 *  9 |           :                              |            |
 * 10 |       A --+--→                           |            |
 * 11 |           :                              |            |
 * 12 |           :                              |            |
 * 13 |        ←--+-- 1                          |            |
 * 14 |           :                              |        3 --+--→
 * 15 |           :                              |            |
 * 16 |           :                              |            |
 * 17 |       5 --+-------------------------------------------+--→
 * 18 |           :                              |            |
 * 19 |           :                  4           |            |
 * 20 |           :                  |           |            |
 *     ______________________________+___________| ____________
 *                                   |           |
 *                                   ↓           ↓
 */
/**
 * Viewport plugin.
 *
 * This plugin manages all things related to all viewport states.
 *
 */
class SheetViewPlugin extends UIPlugin {
    static getters = [
        "getColIndex",
        "getRowIndex",
        "getActiveMainViewport",
        "getSheetViewDimension",
        "getSheetViewDimensionWithHeaders",
        "getMainViewportRect",
        "isVisibleInViewport",
        "getEdgeScrollCol",
        "getEdgeScrollRow",
        "getVisibleFigures",
        "getVisibleRect",
        "getVisibleRectWithoutHeaders",
        "getVisibleCellPositions",
        "getColRowOffsetInViewport",
        "getMainViewportCoordinates",
        "getActiveSheetScrollInfo",
        "getSheetViewVisibleCols",
        "getSheetViewVisibleRows",
        "getFrozenSheetViewRatio",
        "isPixelPositionVisible",
        "getColDimensionsInViewport",
        "getRowDimensionsInViewport",
        "getAllActiveViewportsZonesAndRect",
        "getRect",
        "getFigureUI",
        "getPositionAnchorOffset",
        "getGridOffset",
        "getMaximumSheetOffset",
    ];
    viewports = {};
    /**
     * The viewport dimensions are usually set by one of the components
     * (i.e. when grid component is mounted) to properly reflect its state in the DOM.
     * In the absence of a component (standalone model), is it mandatory to set reasonable default values
     * to ensure the correct operation of this plugin.
     */
    sheetViewWidth = getDefaultSheetViewSize();
    sheetViewHeight = getDefaultSheetViewSize();
    gridOffsetX = 0;
    gridOffsetY = 0;
    sheetsWithDirtyViewports = new Set();
    shouldAdjustViewports = false;
    // ---------------------------------------------------------------------------
    // Command Handling
    // ---------------------------------------------------------------------------
    allowDispatch(cmd) {
        switch (cmd.type) {
            case "SET_VIEWPORT_OFFSET":
                return this.chainValidations(this.checkScrollingDirection, this.checkIfViewportsWillChange)(cmd);
            case "RESIZE_SHEETVIEW":
                return this.chainValidations(this.checkValuesAreDifferent, this.checkPositiveDimension)(cmd);
            default:
                return "Success" /* CommandResult.Success */;
        }
    }
    handleEvent(event) {
        const sheetId = this.getters.getActiveSheetId();
        if (event.options.scrollIntoView) {
            let { col, row } = findCellInNewZone(event.previousAnchor.zone, event.anchor.zone);
            if (event.mode === "updateAnchor") {
                const oldZone = event.previousAnchor.zone;
                const newZone = event.anchor.zone;
                // altering a zone should not move the viewport in a dimension that wasn't changed
                const { top, bottom, left, right } = this.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);
            if (!this.sheetsWithDirtyViewports.has(sheetId)) {
                this.refreshViewport(this.getters.getActiveSheetId(), { col, row });
            }
        }
    }
    handle(cmd) {
        // changing the evaluation can hide/show rows because of data filters
        if (invalidateEvaluationCommands.has(cmd.type)) {
            for (const sheetId of this.getters.getSheetIds()) {
                this.sheetsWithDirtyViewports.add(sheetId);
            }
        }
        switch (cmd.type) {
            case "START":
                this.selection.observe(this, {
                    handleEvent: this.handleEvent.bind(this),
                });
                this.resetViewports(this.getters.getActiveSheetId());
                break;
            case "UNDO":
            case "REDO":
                this.cleanViewports();
                for (const sheetId of this.getters.getSheetIds()) {
                    this.sheetsWithDirtyViewports.add(sheetId);
                }
                this.shouldAdjustViewports = true;
                break;
            case "RESIZE_SHEETVIEW":
                this.resizeSheetView(cmd.height, cmd.width, cmd.gridOffsetX, cmd.gridOffsetY);
                break;
            case "SET_VIEWPORT_OFFSET":
                this.setSheetViewOffset(cmd.offsetX, cmd.offsetY);
                break;
            case "SHIFT_VIEWPORT_DOWN":
                const 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_TABLE":
            case "UPDATE_TABLE":
            case "UPDATE_FILTER":
            case "UNFREEZE_ROWS":
            case "UNFREEZE_COLUMNS":
            case "FREEZE_COLUMNS":
            case "FREEZE_ROWS":
            case "UNFREEZE_COLUMNS_ROWS":
            case "REMOVE_COLUMNS_ROWS":
            case "RESIZE_COLUMNS_ROWS":
            case "HIDE_COLUMNS_ROWS":
            case "ADD_COLUMNS_ROWS":
            case "UNHIDE_COLUMNS_ROWS":
            case "UNGROUP_HEADERS":
            case "GROUP_HEADERS":
            case "FOLD_HEADER_GROUP":
            case "UNFOLD_HEADER_GROUP":
            case "FOLD_HEADER_GROUPS_IN_ZONE":
            case "UNFOLD_HEADER_GROUPS_IN_ZONE":
            case "UNFOLD_ALL_HEADER_GROUPS":
            case "FOLD_ALL_HEADER_GROUPS":
                this.sheetsWithDirtyViewports.add(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 || cmd.style?.fontSize !== undefined) {
                    for (const sheetId of this.getters.getSheetIds()) {
                        this.sheetsWithDirtyViewports.add(sheetId);
                    }
                }
                break;
            case "DELETE_SHEET":
                this.cleanViewports();
                this.sheetsWithDirtyViewports.delete(cmd.sheetId);
                break;
            case "ACTIVATE_SHEET":
                this.sheetsWithDirtyViewports.add(cmd.sheetIdTo);
                break;
            case "SCROLL_TO_CELL":
                this.refreshViewport(this.getters.getActiveSheetId(), { col: cmd.col, row: cmd.row });
                break;
        }
    }
    finalize() {
        for (const sheetId of this.sheetsWithDirtyViewports) {
            this.resetViewports(sheetId);
            if (this.shouldAdjustViewports) {
                const position = this.getters.getSheetPosition(sheetId);
                this.getSubViewports(sheetId).forEach((viewport) => {
                    viewport.adjustPosition(position);
                });
            }
        }
        this.sheetsWithDirtyViewports = new Set();
        this.shouldAdjustViewports = false;
        this.setViewports();
    }
    setViewports() {
        const sheetIds = this.getters.getSheetIds();
        for (const sheetId of sheetIds) {
            if (!this.viewports[sheetId]?.bottomRight) {
                this.resetViewports(sheetId);
            }
        }
    }
    // ---------------------------------------------------------------------------
    // Getters
    // ---------------------------------------------------------------------------
    /**
     * Return the index of a column given an offset x, based on the viewport left
     * visible cell.
     * It returns -1 if no column is found.
     */
    getColIndex(x) {
        const sheetId = this.getters.getActiveSheetId();
        return Math.max(...this.getSubViewports(sheetId).map((viewport) => viewport.getColIndex(x)));
    }
    /**
     * Return the index of a row given an offset y, based on the viewport top
     * visible cell.
     * It returns -1 if no row is found.
     */
    getRowIndex(y) {
        const sheetId = this.getters.getActiveSheetId();
        return Math.max(...this.getSubViewports(sheetId).map((viewport) => viewport.getRowIndex(y)));
    }
    getSheetViewDimensionWithHeaders() {
        return {
            width: this.sheetViewWidth + this.gridOffsetX,
            height: this.sheetViewHeight + this.gridOffsetY,
        };
    }
    getSheetViewDimension() {
        return {
            width: this.sheetViewWidth,
            height: this.sheetViewHeight,
        };
    }
    getGridOffset() {
        return { x: this.gridOffsetX, y: this.gridOffsetY };
    }
    /** type as pane, not viewport but basically pane extends viewport */
    getActiveMainViewport() {
        const sheetId = this.getters.getActiveSheetId();
        return this.getMainViewport(sheetId);
    }
    /**
     * 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.
     */
    getActiveSheetScrollInfo() {
        const sheetId = this.getters.getActiveSheetId();
        const viewport = this.getMainInternalViewport(sheetId);
        return {
            scrollX: viewport.offsetX,
            scrollY: viewport.offsetY,
        };
    }
    getSheetViewVisibleCols() {
        const sheetId = this.getters.getActiveSheetId();
        const viewports = this.getSubViewports(sheetId);
        //TODO ake another commit to eimprove this
        return [...new Set(viewports.map((v) => range(v.left, v.right + 1)).flat())].filter((col) => 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));
    }
    /**
     * Get the positions of all the cells that are visible in the viewport, taking merges into account.
     */
    getVisibleCellPositions() {
        const visibleCols = this.getSheetViewVisibleCols();
        const visibleRows = this.getSheetViewVisibleRows();
        const sheetId = this.getters.getActiveSheetId();
        const positions = [];
        for (const col of visibleCols) {
            for (const row of visibleRows) {
                const position = { sheetId, col, row };
                const mainPosition = this.getters.getMainCellPosition(position);
                if (mainPosition.row !== row || mainPosition.col !== col) {
                    continue;
                }
                positions.push(position);
            }
        }
        return positions;
    }
    /**
     * 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);
        const { 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),
            maxOffsetY: Math.max(0, height - viewport.viewportHeight),
        };
    }
    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 += this.getters.getHeaderSize(sheetId, dimension, i);
        }
        return offset;
    }
    /**
     * Check if a given position is visible in the viewport.
     */
    isVisibleInViewport({ sheetId, col, row }) {
        return this.getSubViewports(sheetId).some((pane) => pane.isVisible(col, row));
    }
    // => returns 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 rect = this.getVisibleRectWithoutHeaders(zone);
        return { ...rect, x: rect.x + this.gridOffsetX, y: rect.y + this.gridOffsetY };
    }
    /**
     * Computes the coordinates and size to draw the zone without taking the grid offset into account
     */
    getVisibleRectWithoutHeaders(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();
        const rect = this.mapViewportsToRect(sheetId, (viewport) => viewport.getFullRect(zone));
        return { ...rect, x: rect.x + this.gridOffsetX, y: rect.y + this.gridOffsetY };
    }
    /**
     * Computes the actual size and position (:Rect) of the zone on the canvas
     * regardless of the viewport dimensions.
     */
    getRectWithoutHeaders(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 };
    }
    /**
     * Returns the size, start and end coordinates of a column relative to the left
     * column of the current viewport
     */
    getColDimensionsInViewport(sheetId, col) {
        const { top } = this.getMainInternalViewport(sheetId);
        const zone = {
            left: col,
            right: col,
            top: top,
            bottom: top,
        };
        const { x, width } = this.getVisibleRect(zone);
        const start = x - this.gridOffsetX;
        return { start, size: width, end: start + width };
    }
    /**
     * Returns the size, start and end coordinates of a row relative to the top row
     * of the current viewport
     */
    getRowDimensionsInViewport(sheetId, row) {
        const { left } = this.getMainInternalViewport(sheetId);
        const zone = {
            left: 0,
            right: left,
            top: row,
            bottom: row,
        };
        const { y, height } = this.getVisibleRect(zone);
        const start = y - this.gridOffsetY;
        return { start, size: height, end: start + height };
    }
    getAllActiveViewportsZonesAndRect() {
        const sheetId = this.getters.getActiveSheetId();
        return this.getSubViewports(sheetId).map((viewport) => {
            return {
                zone: viewport,
                rect: {
                    x: viewport.offsetCorrectionX + this.gridOffsetX,
                    y: viewport.offsetCorrectionY + this.gridOffsetY,
                    ...viewport.getMaxSize(),
                },
            };
        });
    }
    // ---------------------------------------------------------------------------
    // Private
    // ---------------------------------------------------------------------------
    ensureMainViewportExist(sheetId) {
        if (!this.viewports[sheetId]) {
            this.resetViewports(sheetId);
        }
    }
    getSubViewports(sheetId) {
        this.ensureMainViewportExist(sheetId);
        return Object.values(this.viewports[sheetId]).filter(isDefined);
    }
    checkPositiveDimension(cmd) {
        if (cmd.width < 0 || cmd.height < 0) {
            return "InvalidViewportSize" /* CommandResult.InvalidViewportSize */;
        }
        return "Success" /* CommandResult.Success */;
    }
    checkValuesAreDifferent(cmd) {
        const { height, width } = this.getSheetViewDimension();
        if (cmd.gridOffsetX === this.gridOffsetX &&
            cmd.gridOffsetY === this.gridOffsetY &&
            cmd.width === width &&
            cmd.height === height) {
            return "ValuesNotChanged" /* CommandResult.ValuesNotChanged */;
        }
        return "Success" /* CommandResult.Success */;
    }
    checkScrollingDirection({ offsetX, offsetY, }) {
        const pane = this.getMainInternalViewport(this.getters.getActiveSheetId());
        if ((!pane.canScrollHorizontally && offsetX > 0) ||
            (!pane.canScrollVertically && offsetY > 0)) {
            return "InvalidScrollingDirection" /* CommandResult.InvalidScrollingDirection */;
        }
        return "Success" /* CommandResult.Success */;
    }
    checkIfViewportsWillChange({ offsetX, offsetY }) {
        const sheetId = this.getters.getActiveSheetId();
        const { maxOffsetX, maxOffsetY } = this.getMaximumSheetOffset();
        const willScroll = this.getSubViewports(sheetId).some((viewport) => viewport.willNewOffsetScrollViewport(clip(offsetX, 0, maxOffsetX), clip(offsetY, 0, maxOffsetY)));
        return willScroll ? "Success" /* CommandResult.Success */ : "ViewportScrollLimitsReached" /* CommandResult.ViewportScrollLimitsReached */;
    }
    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;
    }
    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) {
        return {
            x: this.viewports[sheetId]?.bottomRight.offsetX || 0,
            y: this.viewports[sheetId]?.bottomRight.offsetY || 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);
        }
    }
    /**
     * Return the index of a col given a negative offset x, based on the main viewport top
     * visible cell of the main viewport.
     * It returns -1 if no col is found.
     */
    getColIndexLeftOfMainViewport(x) {
        if (x >= 0)
            return -1;
        const sheetId = this.getters.getActiveSheetId();
        let col = this.getActiveMainViewport().left;
        let colStart = -this.getActiveSheetScrollInfo().scrollX - this.getters.getColRowOffset("COL", col, 0);
        for (; x < colStart; col--) {
            colStart -= this.getters.getColSize(sheetId, col - 1);
        }
        return Math.max(col, 0);
    }
    /**
     * Return the index of a row given a negative offset y, based on the main viewport top
     * visible cell of the main viewport.
     * It returns -1 if no row is found.
     */
    getRowIndexTopOfMainViewport(y) {
        if (y >= 0)
            return -1;
        const sheetId = this.getters.getActiveSheetId();
        let row = this.getActiveMainViewport().top;
        let rowStart = -this.getActiveSheetScrollInfo().scrollY - this.getters.getColRowOffset("ROW", row, 0);
        for (; y < rowStart; row--) {
            rowStart -= this.getters.getRowSize(sheetId, row - 1);
        }
        return Math.max(row, 0);
    }
    getVisibleFigures() {
        const sheetId = this.getters.getActiveSheetId();
        const result = [];
        const figures = this.getters.getFigures(sheetId);
        const { scrollX, scrollY } = this.getters.getActiveSheetScrollInfo();
        const { x: offsetCorrectionX, y: offsetCorrectionY } = this.getMainViewportCoordinates();
        const { width, height } = this.getters.getSheetViewDimensionWithHeaders();
        for (const figure of figures) {
            const figureUI = this.getFigureUI(sheetId, figure);
            const { x, y } = figureUI;
            if (x >= offsetCorrectionX &&
                (x + figure.width < scrollX + offsetCorrectionX || x > width + scrollX + offsetCorrectionX)) {
                continue;
            }
            else if (y >= offsetCorrectionY &&
                (y + figure.height < scrollY + offsetCorrectionY ||
                    y > height + scrollY + offsetCorrectionY)) {
                continue;
            }
            result.push(figureUI);
        }
        return result;
    }
    getFigureUI(sheetId, figure) {
        const x = figure.offset.x + this.getters.getColDimensions(sheetId, figure.col).start;
        const y = figure.offset.y + this.getters.getRowDimensions(sheetId, figure.row).start;
        return { ...figure, x, y };
    }
    getPositionAnchorOffset(position) {
        const { scrollX, scrollY } = this.getters.getActiveSheetScrollInfo();
        const x = position.x - scrollX;
        const y = position.y - scrollY;
        const col = x >= 0 ? this.getColIndex(x) : this.getColIndexLeftOfMainViewport(x);
        const row = y >= 0 ? this.getRowIndex(y) : this.getRowIndexTopOfMainViewport(y);
        const { x: colX, y: rowY } = this.getRect(positionToZone({ col, row }));
        return {
            col,
            row,
            offset: {
                x: Math.max(x - colX + this.gridOffsetX, 0),
                y: Math.max(y - rowY + this.gridOffsetY, 0),
            },
        };
    }
    isPixelPositionVisible(position) {
        const { scrollX, scrollY } = this.getters.getActiveSheetScrollInfo();
        const { x: mainViewportX, y: mainViewportY } = this.getters.getMainViewportCoordinates();
        const { width, height } = this.getters.getSheetViewDimension();
        if (position.x >= mainViewportX &&
            (position.x < mainViewportX + scrollX || position.x > width + scrollX + mainViewportX)) {
            return false;
        }
        if (position.y >= mainViewportY &&
            (position.y < mainViewportY + scrollY || position.y > height + scrollY + mainViewportY)) {
            return false;
        }
        return true;
    }
    getFrozenSheetViewRatio(sheetId) {
        const { xSplit, ySplit } = this.getters.getPaneDivisions(sheetId);
        const offsetCorrectionX = this.getters.getColDimensions(sheetId, xSplit).start;
        const offsetCorrectionY = this.getters.getRowDimensions(sheetId, ySplit).start;
        const width = this.sheetViewWidth + this.gridOffsetX;
        const height = this.sheetViewHeight + this.gridOffsetY;
        return { xRatio: offsetCorrectionX / width, yRatio: offsetCorrectionY / height };
    }
    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, y, width: width - x, height: height - y };
    }
}

class CarouselUIPlugin extends UIPlugin {
    static getters = [
        "getSelectedCarouselItem",
        "getChartFromFigureId",
        "getChartIdFromFigureId",
    ];
    uuidGenerator = new UuidGenerator();
    carouselStates = {};
    allowDispatch(cmd) {
        switch (cmd.type) {
            case "ADD_FIGURE_CHART_TO_CAROUSEL":
                if (!this.getters.doesCarouselExist(cmd.carouselFigureId) ||
                    this.getters.getFigure(cmd.sheetId, cmd.chartFigureId)?.tag !== "chart") {
                    return "InvalidFigureId" /* CommandResult.InvalidFigureId */;
                }
                return "Success" /* CommandResult.Success */;
            case "ADD_NEW_CHART_TO_CAROUSEL":
                if (!this.getters.doesCarouselExist(cmd.figureId)) {
                    return "InvalidFigureId" /* CommandResult.InvalidFigureId */;
                }
                return "Success" /* CommandResult.Success */;
            case "UPDATE_CAROUSEL_ACTIVE_ITEM":
                if (!this.getters.doesCarouselExist(cmd.figureId)) {
                    return "InvalidFigureId" /* CommandResult.InvalidFigureId */;
                }
                else if (!this.getters.getCarousel(cmd.figureId).items.some((item) => deepEquals(item, cmd.item))) {
                    return "InvalidCarouselItem" /* CommandResult.InvalidCarouselItem */;
                }
                return "Success" /* CommandResult.Success */;
        }
        return "Success" /* CommandResult.Success */;
    }
    handle(cmd) {
        switch (cmd.type) {
            case "ADD_NEW_CHART_TO_CAROUSEL":
                this.addNewChartToCarousel(cmd.figureId, cmd.sheetId);
                break;
            case "ADD_FIGURE_CHART_TO_CAROUSEL":
                this.addFigureChartToCarousel(cmd.carouselFigureId, cmd.chartFigureId, cmd.sheetId);
                break;
            case "UPDATE_CAROUSEL_ACTIVE_ITEM":
                this.carouselStates[cmd.figureId] = this.getCarouselItemId(cmd.item);
                break;
            case "POPOUT_CHART_FROM_CAROUSEL":
                this.popOutChartFromCarousel(cmd.carouselId, cmd.chartId, cmd.sheetId);
                break;
            case "DELETE_FIGURE":
                delete this.carouselStates[cmd.figureId];
                break;
            case "UPDATE_CAROUSEL":
                this.fixWrongCarouselState(cmd.figureId);
                break;
            case "DELETE_CHART":
            case "UNDO":
            case "REDO":
            case "DELETE_SHEET":
                for (const figureId in this.carouselStates) {
                    this.fixWrongCarouselState(figureId);
                }
                break;
        }
    }
    popOutChartFromCarousel(carouselId, chartId, sheetId) {
        const carousel = this.getters.getCarousel(carouselId);
        if (!carousel) {
            return;
        }
        const figure = this.getters.getFigure(sheetId, carouselId);
        const chartDefinition = this.getters.getChartDefinition(chartId);
        if (!chartDefinition || !figure) {
            return;
        }
        const figureUI = this.getters.getFigureUI(sheetId, figure);
        const newAnchor = this.getters.getPositionAnchorOffset({
            x: figureUI.x + 50,
            y: figureUI.y + 50,
        });
        const newChartFigureId = this.uuidGenerator.smallUuid();
        this.dispatch("CREATE_CHART", {
            ...newAnchor,
            chartId: this.uuidGenerator.smallUuid(),
            figureId: newChartFigureId,
            sheetId,
            size: { width: figure.width, height: figure.height },
            definition: { ...chartDefinition },
        });
        const items = carousel.items.filter((item) => item.type !== "chart" || item.chartId !== chartId);
        this.dispatch("UPDATE_CAROUSEL", {
            sheetId,
            figureId: carouselId,
            definition: { ...carousel, items },
        });
        this.dispatch("SELECT_FIGURE", { figureId: newChartFigureId });
    }
    getSelectedCarouselItem(figureId) {
        const carousel = this.getters.getCarousel(figureId);
        if (!carousel.items.length) {
            return undefined;
        }
        return this.carouselStates[figureId]
            ? carousel.items.find((item) => this.getCarouselItemId(item) === this.carouselStates[figureId])
            : carousel.items[0];
    }
    getChartFromFigureId(figureId) {
        const sheetId = this.getters.getFigureSheetId(figureId);
        if (!sheetId) {
            return undefined;
        }
        const chartId = this.getChartIdFromFigureId(figureId);
        return chartId ? this.getters.getChart(chartId) : undefined;
    }
    getChartIdFromFigureId(figureId) {
        const sheetId = this.getters.getFigureSheetId(figureId);
        if (!sheetId) {
            return undefined;
        }
        const figure = this.getters.getFigure(sheetId, figureId);
        if (!figure || (figure.tag !== "chart" && figure.tag !== "carousel")) {
            return undefined;
        }
        if (figure.tag === "carousel") {
            const carouselItem = this.getSelectedCarouselItem(figureId);
            return carouselItem?.type === "chart" ? carouselItem.chartId : undefined;
        }
        return this.getters
            .getChartIds(sheetId)
            .find((chartId) => this.getters.getFigureIdFromChartId(chartId) === figureId);
    }
    fixWrongCarouselState(figureId) {
        if (!this.getters.doesCarouselExist(figureId)) {
            delete this.carouselStates[figureId];
            return;
        }
        const carousel = this.getters.getCarousel(figureId);
        if (carousel.items.length === 0) {
            delete this.carouselStates[figureId];
        }
        else if (!this.carouselStates[figureId]) {
            this.carouselStates[figureId] = this.getCarouselItemId(carousel.items[0]);
        }
        else if (!carousel.items.some((item) => this.getCarouselItemId(item) === this.carouselStates[figureId])) {
            this.carouselStates[figureId] = this.getCarouselItemId(carousel.items[0]);
        }
    }
    addNewChartToCarousel(figureId, sheetId) {
        const carousel = this.getters.getCarousel(figureId);
        const chartId = this.uuidGenerator.smallUuid();
        this.dispatch("CREATE_CHART", {
            chartId,
            figureId,
            sheetId,
            definition: CAROUSEL_DEFAULT_CHART_DEFINITION,
        });
        const definition = {
            ...carousel,
            items: [...carousel.items, { type: "chart", chartId }],
        };
        this.dispatch("UPDATE_CAROUSEL", { sheetId, figureId, definition });
    }
    addFigureChartToCarousel(figureId, chartFigureId, sheetId) {
        const chartId = this.getChartIdFromFigureId(chartFigureId);
        if (!chartId) {
            return;
        }
        const carousel = this.getters.getCarousel(figureId);
        const definition = {
            ...carousel,
            items: [...carousel.items, { type: "chart", chartId }],
        };
        this.dispatch("UPDATE_CAROUSEL", { sheetId, figureId, definition });
        this.dispatch("UPDATE_CHART", {
            sheetId,
            chartId,
            figureId,
            definition: this.getters.getChartDefinition(chartId),
        });
        this.dispatch("DELETE_FIGURE", { sheetId, figureId: chartFigureId });
    }
    getCarouselItemId(item) {
        return item.type === "chart" ? item.chartId : "carouselDataView";
    }
}

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

const corePluginRegistry = new Registry()
    .add("settings", SettingsPlugin)
    .add("sheet", SheetPlugin)
    .add("header grouping", HeaderGroupingPlugin)
    .add("header visibility", HeaderVisibilityPlugin)
    .add("tables", TablePlugin)
    .add("dataValidation", DataValidationPlugin)
    .add("cell", CellPlugin)
    .add("merge", MergePlugin)
    .add("headerSize", HeaderSizePlugin)
    .add("borders", BordersPlugin)
    .add("conditional formatting", ConditionalFormatPlugin)
    .add("figures", FigurePlugin)
    .add("chart", ChartPlugin)
    .add("carousel", CarouselPlugin)
    .add("image", ImagePlugin)
    .add("pivot_core", PivotCorePlugin)
    .add("spreadsheet_pivot_core", SpreadsheetPivotCorePlugin)
    .add("tableStyle", TableStylePlugin);
// Plugins which handle a specific feature, without handling any core commands
const featurePluginRegistry = new Registry()
    .add("ui_sheet", SheetUIPlugin)
    .add("ui_options", UIOptionsPlugin)
    .add("autofill", AutofillPlugin)
    .add("sort", SortPlugin)
    .add("automatic_sum", AutomaticSumPlugin)
    .add("format", FormatPlugin)
    .add("insert_pivot", InsertPivotPlugin)
    .add("pivot_presence", PivotPresencePlugin)
    .add("split_to_columns", SplitToColumnsPlugin)
    .add("subtotal_evaluation", SubtotalEvaluationPlugin)
    .add("collaborative", CollaborativePlugin)
    .add("history", HistoryPlugin)
    .add("data_cleanup", DataCleanupPlugin)
    .add("table_autofill", TableAutofillPlugin)
    .add("table_ui_resize", TableResizeUI)
    .add("datavalidation_insert", DataValidationInsertionPlugin)
    .add("checkbox_toggle", CheckboxTogglePlugin)
    .add("dynamic_translate", DynamicTranslate)
    .add("geo_features", GeoFeaturePlugin);
// Plugins which have a state, but which should not be shared in collaborative
const statefulUIPluginRegistry = new Registry()
    .add("selection", GridSelectionPlugin)
    .add("evaluation_filter", FilterEvaluationPlugin)
    .add("header_visibility_ui", HeaderVisibilityUIPlugin)
    .add("cell_computed_style", CellComputedStylePlugin)
    .add("table_computed_style", TableComputedStylePlugin)
    .add("header_positions", HeaderPositionsUIPlugin)
    .add("viewport", SheetViewPlugin)
    .add("clipboard", ClipboardPlugin)
    .add("carousel_ui", CarouselUIPlugin);
// Plugins which have a derived state from core data
const coreViewsPluginRegistry = new Registry()
    .add("evaluation", EvaluationPlugin)
    .add("evaluation_chart", EvaluationChartPlugin)
    .add("evaluation_cf", EvaluationConditionalFormatPlugin)
    .add("row_size", HeaderSizeUIPlugin)
    .add("data_validation_ui", EvaluationDataValidationPlugin)
    .add("dynamic_tables", DynamicTablesPlugin)
    .add("custom_colors", CustomColorsPlugin)
    .add("pivot_ui", PivotUIPlugin)
    .add("cell_icon", CellIconPlugin);

autoCompleteProviders.add("dataValidation", {
    displayAllOnInitialContent: true,
    canBeToggled: false,
    getProposals(tokenAtCursor, content) {
        if (isFormula(content)) {
            return [];
        }
        if (!this.composer.currentEditedCell) {
            return [];
        }
        const position = this.composer.currentEditedCell;
        const rule = this.getters.getValidationRuleForCell(position);
        if (!rule ||
            (rule.criterion.type !== "isValueInList" && rule.criterion.type !== "isValueInRange")) {
            return [];
        }
        const sheetId = this.composer.currentEditedCell.sheetId;
        const values = rule.criterion.type === "isValueInRange"
            ? this.getters.getDataValidationRangeValues(sheetId, rule.criterion)
            : rule.criterion.values.map((value) => ({ label: value, value }));
        const isChip = rule.criterion.displayStyle === "chip";
        if (!isChip) {
            return values.map((value) => ({
                text: value.value,
                fuzzySearchKey: value.label,
                htmlContent: [{ value: value.label }],
            }));
        }
        const colors = rule.criterion.colors;
        return values.map((value) => {
            const color = colors?.[value.value];
            return {
                text: value.value,
                htmlContent: [
                    {
                        value: value.label,
                        color: color ? chipTextColor(color) : undefined,
                        backgroundColor: color || GRAY_200,
                        classes: ["badge rounded-pill fs-6 fw-normal w-100 mt-1 text-start"],
                    },
                ],
                fuzzySearchKey: value.label,
            };
        });
    },
    selectProposal(tokenAtCursor, value) {
        this.composer.setCurrentContent(value);
        this.composer.stopEdition();
    },
});

autoCompleteProviders.add("functions", {
    sequence: 100,
    autoSelectFirstProposal: true,
    maxDisplayedProposals: 10,
    getProposals(tokenAtCursor) {
        if (tokenAtCursor.type !== "SYMBOL") {
            return [];
        }
        const searchTerm = tokenAtCursor.value;
        if (!isFormula(this.composer.currentContent)) {
            return [];
        }
        const values = Object.entries(functionRegistry.content)
            .filter(([_, { hidden }]) => !hidden)
            .map(([text, { description }]) => {
            return {
                text,
                description,
                htmlContent: getHtmlContentFromPattern(searchTerm, text, COMPOSER_ASSISTANT_COLOR, "o-semi-bold"),
            };
        })
            .sort((a, b) => {
            return a.text.length - b.text.length || a.text.localeCompare(b.text);
        });
        return values;
    },
    selectProposal(tokenAtCursor, value) {
        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.composer.currentTokens;
        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.composer.changeComposerCursorSelection(start, end);
        this.composer.replaceComposerCursorSelection(value);
    },
});

autoCompleteProviders.add("pivot_ids", {
    sequence: 50,
    autoSelectFirstProposal: true,
    getProposals(tokenAtCursor) {
        const functionContext = tokenAtCursor.functionContext;
        const pivotFunction = ["PIVOT.VALUE", "PIVOT.HEADER", "PIVOT"];
        if (!functionContext ||
            !pivotFunction.includes(functionContext.parent.toUpperCase()) ||
            functionContext.argPosition !== 0) {
            return;
        }
        const pivotIds = this.getters.getPivotIds();
        if (pivotIds.includes(tokenAtCursor.value)) {
            return;
        }
        return pivotIds
            .map((pivotId) => {
            const definition = this.getters.getPivotCoreDefinition(pivotId);
            const formulaId = this.getters.getPivotFormulaId(pivotId);
            const str = `${formulaId}`;
            return {
                text: str,
                description: definition.name,
                htmlContent: [{ value: str, color: tokenColors.NUMBER }],
                fuzzySearchKey: str + definition.name,
                alwaysExpanded: true,
            };
        })
            .filter(isDefined);
    },
    selectProposal: insertTokenAtArgStartingPosition,
});
autoCompleteProviders.add("pivot_measures", {
    sequence: 50,
    autoSelectFirstProposal: true,
    getProposals(tokenAtCursor) {
        const functionContext = tokenAtCursor.functionContext;
        if (functionContext?.parent.toUpperCase() !== "PIVOT.VALUE" ||
            functionContext.argPosition !== 1) {
            return [];
        }
        const pivotFormulaId = extractFormulaIdFromToken(tokenAtCursor);
        const pivotId = this.getters.getPivotId(pivotFormulaId);
        if (!pivotId || !this.getters.isExistingPivot(pivotId)) {
            return [];
        }
        const pivot = this.getters.getPivot(pivotId);
        pivot.init();
        if (!pivot.isValid()) {
            return [];
        }
        return pivot.definition.measures
            .map((measure) => {
            if (measure.fieldName === "__count") {
                const text = '"__count"';
                return {
                    text,
                    description: _t("Count"),
                    htmlContent: [{ value: text, color: tokenColors.STRING }],
                    fuzzySearchKey: _t("Count") + text,
                };
            }
            return makeMeasureProposal(measure);
        })
            .filter(isDefined);
    },
    selectProposal: insertTokenAtArgStartingPosition,
});
autoCompleteProviders.add("pivot_group_fields", {
    sequence: 50,
    autoSelectFirstProposal: true,
    getProposals(tokenAtCursor) {
        const functionContext = tokenAtCursor.functionContext;
        if (!functionContext ||
            (!canAutoCompletePivotField(tokenAtCursor) && !canAutoCompletePivotHeaderField(tokenAtCursor))) {
            return;
        }
        const pivotFormulaId = extractFormulaIdFromToken(tokenAtCursor);
        const pivotId = this.getters.getPivotId(pivotFormulaId);
        if (!pivotId || !this.getters.isExistingPivot(pivotId)) {
            return;
        }
        const pivot = this.getters.getPivot(pivotId);
        pivot.init();
        const fields = pivot.getFields();
        const { columns, rows } = pivot.definition;
        let args = functionContext.args;
        if (functionContext?.parent.toUpperCase() === "PIVOT.VALUE") {
            args = args.filter((ast, index) => index % 2 === 0); // keep only the field names
            args = args.slice(1, functionContext.argPosition); // remove the first even argument (the pivot id)
        }
        else {
            args = args.filter((ast, index) => index % 2 === 1); // keep only the field names
        }
        const argGroupBys = args.map((ast) => ast?.value).filter(isDefined);
        const colFields = columns.map((groupBy) => groupBy.nameWithGranularity);
        const rowFields = rows.map((groupBy) => groupBy.nameWithGranularity);
        const proposals = [];
        let previousGroupBy = ["ARG_SEPARATOR", "SPACE"].includes(tokenAtCursor.type)
            ? argGroupBys.at(-1)
            : argGroupBys.at(-2);
        const isPositionalSupported = supportedPivotPositionalFormulaRegistry.get(pivot.type);
        if (isPositionalSupported && previousGroupBy?.startsWith("#")) {
            previousGroupBy = previousGroupBy.slice(1);
        }
        if (previousGroupBy === undefined) {
            proposals.push(colFields[0]);
            proposals.push(rowFields[0]);
        }
        if (rowFields.includes(previousGroupBy)) {
            const nextRowGroupBy = rowFields[rowFields.indexOf(previousGroupBy) + 1];
            proposals.push(nextRowGroupBy);
            proposals.push(colFields[0]);
        }
        if (colFields.includes(previousGroupBy)) {
            const nextGroupBy = colFields[colFields.indexOf(previousGroupBy) + 1];
            proposals.push(nextGroupBy);
        }
        const groupBys = proposals.filter(isDefined);
        return groupBys
            .map((groupBy) => {
            const [fieldName, granularity] = groupBy.split(":");
            const field = fields[fieldName];
            return field ? makeFieldProposal(field, granularity) : undefined;
        })
            .concat(groupBys.map((groupBy) => {
            if (!isPositionalSupported) {
                return undefined;
            }
            const fieldName = groupBy.split(":")[0];
            const field = fields[fieldName];
            if (!field) {
                return undefined;
            }
            const positionalFieldArg = `"#${groupBy}"`;
            return {
                text: positionalFieldArg,
                description: _t("%s (positional)", field.string) + (field.help ? ` (${field.help})` : ""),
                htmlContent: [{ value: positionalFieldArg, color: tokenColors.STRING }],
                fuzzySearchKey: field.string + positionalFieldArg, // search on translated name and on technical name
            };
        }))
            .filter(isDefined);
    },
    selectProposal: insertTokenAtArgStartingPosition,
});
function canAutoCompletePivotField(tokenAtCursor) {
    const functionContext = tokenAtCursor.functionContext;
    return (functionContext?.parent.toUpperCase() === "PIVOT.VALUE" &&
        functionContext.argPosition >= 2 && // the first two arguments are the pivot id and the measure
        functionContext.argPosition % 2 === 0 // only the even arguments are the group bys
    );
}
function canAutoCompletePivotHeaderField(tokenAtCursor) {
    const functionContext = tokenAtCursor.functionContext;
    return (functionContext?.parent.toUpperCase() === "PIVOT.HEADER" &&
        functionContext.argPosition >= 1 && // the first argument is the pivot id
        functionContext.argPosition % 2 === 1 // only the odd arguments are the group bys
    );
}
autoCompleteProviders.add("pivot_group_values", {
    sequence: 50,
    autoSelectFirstProposal: true,
    getProposals(tokenAtCursor) {
        const functionContext = tokenAtCursor.functionContext;
        if (!functionContext ||
            !tokenAtCursor ||
            (!canAutoCompletePivotGroupValue(tokenAtCursor) &&
                !canAutoCompletePivotHeaderGroupValue(tokenAtCursor))) {
            return;
        }
        const pivotFormulaId = extractFormulaIdFromToken(tokenAtCursor);
        const pivotId = this.getters.getPivotId(pivotFormulaId);
        if (!pivotId || !this.getters.isExistingPivot(pivotId)) {
            return;
        }
        const pivot = this.getters.getPivot(pivotId);
        if (!pivot.isValid()) {
            return;
        }
        const argPosition = functionContext.argPosition;
        const groupByField = tokenAtCursor.functionContext?.args[argPosition - 1]?.value;
        if (!groupByField) {
            return;
        }
        let dimension;
        try {
            dimension = pivot.definition.getDimension(groupByField);
        }
        catch (error) {
            return undefined;
        }
        if (dimension.granularity === "month_number") {
            return Object.values(MONTHS).map((monthDisplayName, index) => ({
                text: `${index + 1}`,
                fuzzySearchKey: monthDisplayName.toString(),
                description: monthDisplayName.toString(),
                htmlContent: [{ value: `${index + 1}`, color: tokenColors.NUMBER }],
            }));
        }
        else if (dimension.granularity === "quarter_number") {
            return [1, 2, 3, 4].map((quarter) => ({
                text: `${quarter}`,
                fuzzySearchKey: `${quarter}`,
                description: _t("Quarter %s", quarter),
                htmlContent: [{ value: `${quarter}`, color: tokenColors.NUMBER }],
            }));
        }
        else if (dimension.granularity === "day_of_month") {
            return range(1, 32).map((dayOfMonth) => ({
                text: `${dayOfMonth}`,
                fuzzySearchKey: `${dayOfMonth}`,
                description: "",
                htmlContent: [{ value: `${dayOfMonth}`, color: tokenColors.NUMBER }],
            }));
        }
        else if (dimension.granularity === "iso_week_number") {
            return range(0, 54).map((isoWeekNumber) => ({
                text: `${isoWeekNumber}`,
                fuzzySearchKey: `${isoWeekNumber}`,
                description: "",
                htmlContent: [{ value: `${isoWeekNumber}`, color: tokenColors.NUMBER }],
            }));
        }
        else if (dimension.granularity === "day_of_week") {
            return range(1, 8).map((dayOfWeekNumber) => ({
                text: `${dayOfWeekNumber}`,
                fuzzySearchKey: `${dayOfWeekNumber}`,
                description: "",
                htmlContent: [{ value: `${dayOfWeekNumber}`, color: tokenColors.NUMBER }],
            }));
        }
        else if (dimension.granularity === "hour_number") {
            return range(0, 24).map((hourNumber) => ({
                text: `${hourNumber}`,
                fuzzySearchKey: `${hourNumber}`,
                description: "",
                htmlContent: [{ value: `${hourNumber}`, color: tokenColors.NUMBER }],
            }));
        }
        else if (dimension.granularity === "minute_number") {
            return range(0, 60).map((minuteNumber) => ({
                text: `${minuteNumber}`,
                fuzzySearchKey: `${minuteNumber}`,
                description: "",
                htmlContent: [{ value: `${minuteNumber}`, color: tokenColors.NUMBER }],
            }));
        }
        else if (dimension.granularity === "second_number") {
            return range(0, 60).map((secondNumber) => ({
                text: `${secondNumber}`,
                fuzzySearchKey: `${secondNumber}`,
                description: "",
                htmlContent: [{ value: `${secondNumber}`, color: tokenColors.NUMBER }],
            }));
        }
        return pivot.getPossibleFieldValues(dimension).map(({ value, label }) => {
            const isString = typeof value === "string";
            const text = isString ? `"${value}"` : value.toString();
            const color = isString ? tokenColors.STRING : tokenColors.NUMBER;
            const usedLabel = label === value ? "" : label;
            return {
                text,
                description: usedLabel,
                htmlContent: [{ value: text, color }],
                fuzzySearchKey: text + usedLabel,
            };
        });
    },
    selectProposal: insertTokenAtArgStartingPosition,
});
function canAutoCompletePivotGroupValue(tokenAtCursor) {
    const functionContext = tokenAtCursor.functionContext;
    return (functionContext?.parent.toUpperCase() === "PIVOT.VALUE" &&
        functionContext.argPosition >= 2 && // the first two arguments are the pivot id and the measure
        functionContext.argPosition % 2 === 1 // only the odd arguments are the group by values
    );
}
function canAutoCompletePivotHeaderGroupValue(tokenAtCursor) {
    const functionContext = tokenAtCursor.functionContext;
    return (functionContext?.parent.toUpperCase() === "PIVOT.HEADER" &&
        functionContext.argPosition >= 1 && // the first argument is the pivot id
        functionContext.argPosition % 2 === 0 // only the even arguments are the group by values
    );
}

autoCompleteProviders.add("sheet_names", {
    sequence: 150,
    autoSelectFirstProposal: true,
    getProposals(tokenAtCursor) {
        if (tokenAtCursor.type === "SYMBOL" ||
            (tokenAtCursor.type === "UNKNOWN" && tokenAtCursor.value.startsWith("'"))) {
            return this.getters.getSheetIds().map((sheetId) => {
                const sheetName = getCanonicalSymbolName(this.getters.getSheetName(sheetId));
                return {
                    text: sheetName,
                    fuzzySearchKey: sheetName.startsWith("'") ? sheetName : "'" + sheetName, // typing a single quote is a way to avoid matching function names
                };
            });
        }
        return [];
    },
    selectProposal(tokenAtCursor, value) {
        const start = tokenAtCursor.start;
        const end = tokenAtCursor.end;
        this.composer.changeComposerCursorSelection(start, end);
        this.composer.replaceComposerCursorSelection(value + "!");
    },
});

const clickableCellRegistry = new Registry();
clickableCellRegistry.add("link", {
    condition: (position, getters) => {
        return !!getters.getEvaluatedCell(position).link;
    },
    execute: (position, env, isMiddleClick) => openLink(env.model.getters.getEvaluatedCell(position).link, env, isMiddleClick),
    title: (position, getters) => {
        const link = getters.getEvaluatedCell(position).link;
        if (!link)
            return "";
        if (link.isExternal) {
            return _t("Go to url: %(url)s", { url: link.url });
        }
        else {
            return _t("Go to %(label)s", { label: link.label });
        }
    },
    sequence: 5,
});
clickableCellRegistry.add("dashboard_pivot_sorting", {
    condition: (position, getters) => {
        if (!getters.isDashboard()) {
            return false;
        }
        const pivotCell = getters.getPivotCellFromPosition(position);
        return canSortPivot(getters, position) && pivotCell.type === "MEASURE_HEADER";
    },
    execute: (position, env) => {
        sortPivot(env, position, getNextSortDirection(env.model.getters, position));
    },
    component: ClickableCellSortIcon,
    componentProps: (position, getters) => {
        return {
            position,
            sortDirection: getters.getPivotCellSortDirection(position),
        };
    },
    sequence: 2,
});
const NEXT_SORT_DIRECTION = {
    none: "asc",
    asc: "desc",
    desc: "none",
};
function getNextSortDirection(getters, position) {
    return NEXT_SORT_DIRECTION[getters.getPivotCellSortDirection(position) ?? "none"];
}

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)
    .add("CREATE_TABLE_STYLE", inverseCreateTableStyle)
    .add("ADD_PIVOT", inverseAddPivot)
    .add("RENAME_SHEET", inverseRenameSheet);
for (const cmd of coreTypes.values()) {
    if (!inverseCommandRegistry.contains(cmd)) {
        inverseCommandRegistry.add(cmd, identity);
    }
}
function identity(cmd) {
    return [cmd];
}
function inverseAddPivot(cmd) {
    return [
        {
            type: "REMOVE_PIVOT",
            pivotId: cmd.pivotId,
        },
    ];
}
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,
            sheetName: cmd.sheetName,
        },
    ];
}
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, sheetName: cmd.name }];
}
function inverseDuplicateSheet(cmd) {
    return [{ type: "DELETE_SHEET", sheetId: cmd.sheetIdTo, sheetName: "" }];
}
function inverseRemoveColumnsRows(cmd) {
    const commands = [];
    const elements = [...cmd.elements].sort((a, b) => a - b);
    for (const 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,
            sheetName: cmd.sheetName,
            position,
        });
    }
    return commands;
}
function inverseDeleteSheet(cmd) {
    return [{ type: "CREATE_SHEET", sheetId: cmd.sheetId, position: 1, name: cmd.sheetName }];
}
function inverseCreateFigure(cmd) {
    return [{ type: "DELETE_FIGURE", figureId: cmd.figureId, sheetId: cmd.sheetId }];
}
function inverseCreateChart(cmd) {
    return [{ type: "DELETE_FIGURE", figureId: cmd.figureId, sheetId: cmd.sheetId }];
}
function inverseHideColumnsRows(cmd) {
    return [
        {
            type: "UNHIDE_COLUMNS_ROWS",
            sheetId: cmd.sheetId,
            dimension: cmd.dimension,
            elements: cmd.elements,
        },
    ];
}
function inverseUnhideColumnsRows(cmd) {
    return [
        {
            type: "HIDE_COLUMNS_ROWS",
            sheetId: cmd.sheetId,
            dimension: cmd.dimension,
            elements: cmd.elements,
        },
    ];
}
function inverseCreateTableStyle(cmd) {
    return [{ type: "REMOVE_TABLE_STYLE", tableStyleId: cmd.tableStyleId }];
}
function inverseRenameSheet(cmd) {
    return [
        {
            type: "RENAME_SHEET",
            sheetId: cmd.sheetId,
            oldName: cmd.newName,
            newName: cmd.oldName,
        },
    ];
}

const numberFormatMenuRegistry = new Registry();
numberFormatMenuRegistry
    .add("format_number_automatic", {
    ...formatNumberAutomatic,
    id: "format_number_automatic",
    sequence: 10,
})
    .add("format_number_plain_text", {
    ...formatNumberPlainText,
    id: "format_number_plain_text",
    sequence: 15,
    separator: true,
})
    .add("format_number_number", {
    ...formatNumberNumber,
    id: "format_number_number",
    sequence: 20,
})
    .add("format_number_percent", {
    ...formatNumberPercent,
    id: "format_number_percent",
    sequence: 30,
    separator: true,
})
    .add("format_number_currency", {
    ...formatNumberCurrency,
    id: "format_number_currency",
    sequence: 40,
})
    .add("format_number_accounting", {
    ...formatNumberAccounting,
    id: "format_number_accounting",
    sequence: 45,
})
    .add("format_number_currency_rounded", {
    ...formatNumberCurrencyRounded,
    id: "format_number_currency_rounded",
    sequence: 50,
})
    .add("format_custom_currency", {
    ...formatCustomCurrency,
    id: "format_custom_currency",
    sequence: 60,
    separator: true,
})
    .add("format_number_date", {
    ...formatNumberDate,
    id: "format_number_date",
    sequence: 70,
})
    .add("format_number_time", {
    ...formatNumberTime,
    id: "format_number_time",
    sequence: 80,
})
    .add("format_number_date_time", {
    ...formatNumberDateTime,
    id: "format_number_date_time",
    sequence: 90,
})
    .add("format_number_duration", {
    ...formatNumberDuration,
    id: "format_number_duration",
    sequence: 100,
    separator: true,
})
    .add("more_formats", {
    ...moreFormats,
    id: "more_formats",
    sequence: 120,
});
function getCustomNumberFormats(env) {
    const defaultFormats = new Set(numberFormatMenuRegistry
        .getAll()
        .map((f) => (typeof f.format === "function" ? f.format(env) : f.format)));
    const customFormats = new Map();
    for (const sheetId of env.model.getters.getSheetIds()) {
        const cells = env.model.getters.getEvaluatedCells(sheetId);
        for (const cellId in cells) {
            const cell = cells[cellId];
            if (cell.format && !customFormats.has(cell.format) && !defaultFormats.has(cell.format)) {
                const formatType = getNumberFormatType(cell.format);
                if (formatType === "date" || formatType === "currency") {
                    customFormats.set(cell.format, createFormatActionSpec({
                        descriptionValue: formatType === "currency" ? 1000 : EXAMPLE_DATE,
                        format: cell.format,
                        name: cell.format,
                    }));
                }
            }
        }
    }
    return [...customFormats.values()];
}
const getNumberFormatType = memoize((format) => {
    if (isDateTimeFormat(format)) {
        return "date";
    }
    else if (format.includes("[$")) {
        return "currency";
    }
    return "number";
});
const formatNumberMenuItemSpec = {
    name: _t("More formats"),
    icon: "o-spreadsheet-Icon.NUMBER_FORMATS",
    children: [
        (env) => {
            const customFormats = getCustomNumberFormats(env).map((action) => ({
                ...action,
                sequence: 110,
            }));
            if (customFormats.length > 0) {
                customFormats[customFormats.length - 1].separator = true;
            }
            return createActions([...numberFormatMenuRegistry.getAll(), ...customFormats]);
        },
    ],
};

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

const topbarMenuRegistry = new MenuItemRegistry();
topbarMenuRegistry
    // ---------------------------------------------------------------------
    // FILE MENU ITEMS
    // ---------------------------------------------------------------------
    .add("file", {
    name: _t("File"),
    sequence: 10,
    isReadonlyAllowed: true,
})
    .addChild("settings", ["file"], {
    name: _t("Settings"),
    sequence: 200,
    execute: (env) => env.openSidePanel("Settings"),
    isEnabled: (env) => !env.isSmall,
    icon: "o-spreadsheet-Icon.COG",
})
    // ---------------------------------------------------------------------
    // EDIT MENU ITEMS
    // ---------------------------------------------------------------------
    .add("edit", {
    name: _t("Edit"),
    sequence: 20,
    isReadonlyAllowed: true,
})
    .addChild("undo", ["edit"], {
    ...undo,
    sequence: 10,
})
    .addChild("redo", ["edit"], {
    ...redo,
    sequence: 20,
    separator: true,
})
    .addChild("copy", ["edit"], {
    ...copy,
    sequence: 30,
})
    .addChild("cut", ["edit"], {
    ...cut,
    sequence: 40,
})
    .addChild("paste", ["edit"], {
    ...paste,
    sequence: 50,
})
    .addChild("paste_special", ["edit"], {
    ...pasteSpecial,
    sequence: 60,
    separator: true,
})
    .addChild("paste_special_value", ["edit", "paste_special"], {
    ...pasteSpecialValue,
    sequence: 10,
})
    .addChild("paste_special_format", ["edit", "paste_special"], {
    ...pasteSpecialFormat,
    sequence: 20,
})
    .addChild("edit_table", ["edit"], {
    ...editTable,
    isVisible: SELECTION_CONTAINS_SINGLE_TABLE,
    sequence: 60,
})
    .addChild("find_and_replace", ["edit"], {
    ...findAndReplace,
    sequence: 65,
    separator: true,
})
    .addChild("delete", ["edit"], {
    name: _t("Delete"),
    icon: "o-spreadsheet-Icon.TRASH",
    sequence: 70,
})
    .addChild("edit_delete_cell_values", ["edit", "delete"], {
    ...deleteValues,
    sequence: 10,
})
    .addChild("edit_delete_row", ["edit", "delete"], {
    ...deleteRows,
    sequence: 20,
})
    .addChild("edit_delete_column", ["edit", "delete"], {
    ...deleteCols,
    sequence: 30,
})
    .addChild("edit_delete_cell_shift_up", ["edit", "delete"], {
    ...deleteCellShiftUp,
    sequence: 40,
})
    .addChild("edit_delete_cell_shift_left", ["edit", "delete"], {
    ...deleteCellShiftLeft,
    sequence: 50,
})
    .addChild("edit_unhide_columns", ["edit"], {
    ...unhideAllCols,
    sequence: 80,
})
    .addChild("edit_unhide_rows", ["edit"], {
    ...unhideAllRows,
    sequence: 80,
})
    // ---------------------------------------------------------------------
    // VIEW MENU ITEMS
    // ---------------------------------------------------------------------
    .add("view", {
    name: _t("View"),
    sequence: 30,
    isReadonlyAllowed: true,
})
    .addChild("unfreeze_panes", ["view"], {
    ...unFreezePane,
    sequence: 4,
})
    .addChild("freeze_panes", ["view"], {
    ...freezePane,
    sequence: 5,
})
    .addChild("unfreeze_rows", ["view", "freeze_panes"], {
    ...unFreezeRows,
    sequence: 5,
})
    .addChild("freeze_first_row", ["view", "freeze_panes"], {
    ...freezeFirstRow,
    sequence: 10,
})
    .addChild("freeze_second_row", ["view", "freeze_panes"], {
    ...freezeSecondRow,
    sequence: 15,
})
    .addChild("freeze_current_row", ["view", "freeze_panes"], {
    ...freezeCurrentRow,
    sequence: 20,
    separator: true,
})
    .addChild("unfreeze_columns", ["view", "freeze_panes"], {
    ...unFreezeCols,
    sequence: 25,
})
    .addChild("freeze_first_col", ["view", "freeze_panes"], {
    ...freezeFirstCol,
    sequence: 30,
})
    .addChild("freeze_second_col", ["view", "freeze_panes"], {
    ...freezeSecondCol,
    sequence: 35,
})
    .addChild("freeze_current_col", ["view", "freeze_panes"], {
    ...freezeCurrentCol,
    sequence: 40,
})
    .addChild("group_headers", ["view"], {
    name: _t("Group"),
    sequence: 15,
    separator: true,
    icon: "o-spreadsheet-Icon.PLUS_IN_BOX",
    isVisible: IS_ONLY_ONE_RANGE,
})
    .addChild("group_columns", ["view", "group_headers"], {
    ...groupColumns,
    sequence: 5,
})
    .addChild("ungroup_columns", ["view", "group_headers"], {
    ...ungroupColumns,
    isVisible: (env) => canUngroupHeaders(env, "COL"),
    sequence: 10,
})
    .addChild("group_rows", ["view", "group_headers"], {
    ...groupRows,
    sequence: 15,
})
    .addChild("ungroup_rows", ["view", "group_headers"], {
    ...ungroupRows,
    isVisible: (env) => canUngroupHeaders(env, "ROW"),
    sequence: 20,
})
    .addChild("show", ["view"], {
    name: _t("Show"),
    sequence: 1,
    icon: "o-spreadsheet-Icon.SHOW",
})
    .addChild("view_gridlines", ["view", "show"], {
    ...viewGridlines,
    sequence: 5,
})
    .addChild("view_formulas", ["view", "show"], {
    ...viewFormulas,
    sequence: 10,
})
    .addChild("view_irregularity_map", ["view"], {
    ...irregularityMap,
    sequence: 40,
    separator: true,
})
    // ---------------------------------------------------------------------
    // INSERT MENU ITEMS
    // ---------------------------------------------------------------------
    .add("insert", {
    name: _t("Insert"),
    sequence: 40,
    isReadonlyAllowed: true,
})
    .addChild("insert_row", ["insert"], {
    ...insertRow,
    sequence: 10,
})
    .addChild("insert_row_before", ["insert", "insert_row"], {
    ...topBarInsertRowsBefore,
    sequence: 10,
})
    .addChild("insert_row_after", ["insert", "insert_row"], {
    ...topBarInsertRowsAfter,
    sequence: 20,
})
    .addChild("insert_column", ["insert"], {
    ...insertCol,
    sequence: 20,
})
    .addChild("insert_column_before", ["insert", "insert_column"], {
    ...topBarInsertColsBefore,
    sequence: 10,
})
    .addChild("insert_column_after", ["insert", "insert_column"], {
    ...topBarInsertColsAfter,
    sequence: 20,
})
    .addChild("insert_cell", ["insert"], {
    ...insertCell,
    sequence: 30,
})
    .addChild("insert_cell_down", ["insert", "insert_cell"], {
    ...insertCellShiftDown,
    name: _t("Shift down"),
    sequence: 10,
})
    .addChild("insert_cell_right", ["insert", "insert_cell"], {
    ...insertCellShiftRight,
    name: _t("Shift right"),
    sequence: 20,
})
    .addChild("insert_sheet", ["insert"], {
    ...insertSheet,
    sequence: 40,
    separator: true,
})
    .addChild("insert_chart", ["insert"], {
    ...insertChart,
    sequence: 50,
})
    .addChild("insert_carousel", ["insert"], {
    ...insertCarousel,
    sequence: 51,
})
    .addChild("insert_pivot", ["insert"], {
    ...insertPivot,
    sequence: 52,
})
    .addChild("insert_image", ["insert"], {
    ...insertImage,
    sequence: 55,
})
    .addChild("insert_table", ["insert"], {
    ...insertTable,
    sequence: 57,
})
    .addChild("insert_function", ["insert"], {
    ...insertFunction,
    sequence: 60,
})
    .addChild("insert_function_sum", ["insert", "insert_function"], {
    ...insertFunctionSum,
    sequence: 0,
})
    .addChild("insert_function_average", ["insert", "insert_function"], {
    ...insertFunctionAverage,
    sequence: 10,
})
    .addChild("insert_function_count", ["insert", "insert_function"], {
    ...insertFunctionCount,
    sequence: 20,
})
    .addChild("insert_function_max", ["insert", "insert_function"], {
    ...insertFunctionMax,
    sequence: 30,
})
    .addChild("insert_function_min", ["insert", "insert_function"], {
    ...insertFunctionMin,
    sequence: 40,
    separator: true,
})
    .addChild("categorie_function_all", ["insert", "insert_function"], {
    ...categorieFunctionAll,
    sequence: 50,
})
    .addChild("categories_function_list", ["insert", "insert_function"], categoriesFunctionListMenuBuilder)
    .addChild("insert_link", ["insert"], {
    ...insertLink,
    separator: true,
    sequence: 70,
})
    .addChild("insert_checkbox", ["insert"], {
    ...insertCheckbox,
    sequence: 80,
})
    .addChild("insert_dropdown", ["insert"], {
    ...insertDropdown,
    separator: true,
    sequence: 90,
})
    // ---------------------------------------------------------------------
    // FORMAT MENU ITEMS
    // ---------------------------------------------------------------------
    .add("format", {
    name: _t("Format"),
    sequence: 50,
    isReadonlyAllowed: true,
})
    .addChild("format_number", ["format"], {
    ...formatNumberMenuItemSpec,
    name: _t("Number"),
    sequence: 10,
    separator: true,
})
    .addChild("format_bold", ["format"], {
    ...formatBold,
    sequence: 20,
})
    .addChild("format_italic", ["format"], {
    ...formatItalic,
    sequence: 30,
})
    .addChild("format_underline", ["format"], {
    ...formatUnderline,
    sequence: 40,
})
    .addChild("format_strikethrough", ["format"], {
    ...formatStrikethrough,
    sequence: 50,
    separator: true,
})
    .addChild("format_font_size", ["format"], {
    ...formatFontSize,
    sequence: 60,
    separator: true,
})
    .addChild("format_alignment", ["format"], {
    ...formatAlignment,
    sequence: 70,
})
    .addChild("format_alignment_left", ["format", "format_alignment"], {
    ...formatAlignmentLeft,
    sequence: 10,
})
    .addChild("format_alignment_center", ["format", "format_alignment"], {
    ...formatAlignmentCenter,
    sequence: 20,
})
    .addChild("format_alignment_right", ["format", "format_alignment"], {
    ...formatAlignmentRight,
    sequence: 30,
    separator: true,
})
    .addChild("format_alignment_top", ["format", "format_alignment"], {
    ...formatAlignmentTop,
    sequence: 40,
})
    .addChild("format_alignment_middle", ["format", "format_alignment"], {
    ...formatAlignmentMiddle,
    sequence: 50,
})
    .addChild("format_alignment_bottom", ["format", "format_alignment"], {
    ...formatAlignmentBottom,
    sequence: 60,
    separator: true,
})
    .addChild("format_wrapping", ["format"], {
    ...formatWrappingIcon,
    sequence: 80,
    separator: true,
})
    .addChild("format_wrapping_overflow", ["format", "format_wrapping"], {
    ...formatWrappingOverflow,
    sequence: 10,
})
    .addChild("format_wrapping_wrap", ["format", "format_wrapping"], {
    ...formatWrappingWrap,
    sequence: 20,
})
    .addChild("format_wrapping_clip", ["format", "format_wrapping"], {
    ...formatWrappingClip,
    sequence: 30,
})
    .addChild("format_cf", ["format"], {
    ...formatCF,
    sequence: 90,
    separator: true,
})
    .addChild("format_clearFormat", ["format"], {
    ...clearFormat,
    sequence: 100,
    separator: true,
})
    // ---------------------------------------------------------------------
    // DATA MENU ITEMS
    // ---------------------------------------------------------------------
    .add("data", {
    name: _t("Data"),
    sequence: 60,
    isReadonlyAllowed: true,
})
    .addChild("sort_range", ["data"], {
    ...sortRange,
    sequence: 10,
    separator: true,
})
    .addChild("sort_ascending", ["data", "sort_range"], {
    ...sortAscending,
    sequence: 10,
})
    .addChild("sort_descending", ["data", "sort_range"], {
    ...sortDescending,
    sequence: 20,
})
    .addChild("data_cleanup", ["data"], {
    ...dataCleanup,
    sequence: 15,
})
    .addChild("remove_duplicates", ["data", "data_cleanup"], {
    ...removeDuplicates,
    sequence: 10,
})
    .addChild("trim_whitespace", ["data", "data_cleanup"], {
    ...trimWhitespace,
    sequence: 20,
})
    .addChild("split_to_columns", ["data"], {
    ...splitToColumns,
    sequence: 20,
})
    .addChild("data_validation", ["data"], {
    name: _t("Data Validation"),
    execute: (env) => {
        env.openSidePanel("DataValidation");
    },
    isEnabled: (env) => !env.isSmall,
    icon: "o-spreadsheet-Icon.DATA_VALIDATION",
    sequence: 30,
    separator: true,
})
    .addChild("add_remove_data_filter", ["data"], {
    ...createRemoveFilter,
    sequence: 40,
    separator: true,
})
    .addChild("pivot_data_sources", ["data"], (env) => {
    const sequence = 50;
    const numberOfPivots = env.model.getters.getPivotIds().length;
    return env.model.getters.getPivotIds().map((pivotId, index) => {
        const highlightProvider = {
            get highlights() {
                return getPivotHighlights(env.model.getters, pivotId);
            },
        };
        return {
            id: `item_pivot_${env.model.getters.getPivotFormulaId(pivotId)}`,
            name: env.model.getters.getPivotDisplayName(pivotId),
            sequence: sequence + index / numberOfPivots,
            isReadonlyAllowed: true,
            execute: (env) => env.openSidePanel("PivotSidePanel", { pivotId }),
            isEnabled: (env) => !env.isSmall,
            onStartHover: (env) => env.getStore(HighlightStore).register(highlightProvider),
            onStopHover: (env) => env.getStore(HighlightStore).unRegister(highlightProvider),
            icon: "o-spreadsheet-Icon.PIVOT",
            separator: index === env.model.getters.getPivotIds().length - 1,
            secondaryIcon: (env) => env.model.getters.isPivotUnused(pivotId)
                ? "o-spreadsheet-Icon.UNUSED_PIVOT_WARNING"
                : undefined,
        };
    });
})
    .addChild("reinsert_dynamic_pivot", ["data"], reinsertDynamicPivotMenu)
    .addChild("reinsert_static_pivot", ["data"], reinsertStaticPivotMenu);

class TopBarComponentRegistry extends Registry {
    mapping = {};
    uuidGenerator = new UuidGenerator();
    replace(name, value) {
        const component = { ...value, id: this.uuidGenerator.uuidv4() };
        return super.replace(name, component);
    }
    getAllOrdered() {
        return this.getAll().sort((a, b) => a.sequence - b.sequence);
    }
}
const topbarComponentRegistry = new TopBarComponentRegistry();

class LocalTransportService {
    listeners = [];
    async 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);
    }
}

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

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

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

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

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

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

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

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

      &.o-sheet-name-editable {
        border-radius: 2px;
        border: 2px solid mediumblue;
        /* negative margins so nothing moves when the border is added */
        margin-left: -2px;
        margin-right: -2px;
      }
    }

    .o-sheet-color {
      bottom: 0;
      left: 0;
      height: 6px;
      z-index: 1;
      width: calc(100% - 1px);
    }
  }

  .o-spreadshet-mobile .o-sheet {
    height: ${MOBILE_BOTTOMBAR_HEIGHT}px;
  }
`;
class BottomBarSheet extends Component {
    static template = "o-spreadsheet-BottomBarSheet";
    static props = {
        sheetId: String,
        openContextMenu: Function,
        style: { type: String, optional: true },
        onMouseDown: { type: Function, optional: true },
    };
    static components = { Ripple, ColorPicker };
    static defaultProps = {
        onMouseDown: () => { },
        style: "",
    };
    state = useState({ isEditing: false, pickerOpened: false });
    sheetDivRef = useRef("sheetDiv");
    sheetNameRef = useRef("sheetNameSpan");
    editionState = "initializing";
    DOMFocusableElementStore;
    setup() {
        onPatched(() => {
            if (this.sheetNameRef.el && this.state.isEditing && this.editionState === "initializing") {
                this.editionState = "editing";
                this.focusInputAndSelectContent();
            }
        });
        this.DOMFocusableElementStore = useStore(DOMFocusableElementStore);
        useExternalListener(window, "click", () => (this.state.pickerOpened = false));
        useEffect((sheetId) => {
            if (this.props.sheetId === sheetId) {
                this.scrollToSheet();
            }
        }, () => [this.env.model.getters.getActiveSheetId()]);
    }
    focusInputAndSelectContent() {
        if (!this.state.isEditing || !this.sheetNameRef.el)
            return;
        this.sheetNameRef.el.focus();
        const selection = window.getSelection();
        if (selection && this.sheetNameRef.el.firstChild) {
            selection.setBaseAndExtent(this.sheetNameRef.el.firstChild, 0, this.sheetNameRef.el.firstChild, this.sheetNameRef.el.textContent?.length || 0);
        }
    }
    scrollToSheet() {
        this.sheetDivRef.el?.scrollIntoView?.({
            behavior: "smooth",
            inline: "nearest",
        });
    }
    onFocusOut() {
        if (this.state.isEditing && this.editionState !== "initializing") {
            this.stopEdition();
        }
    }
    onClick() {
        if (!this.env.isMobile()) {
            return;
        }
        this.activateSheet();
    }
    onMouseDown(ev) {
        if (this.env.isMobile()) {
            return;
        }
        this.activateSheet();
        this.props.onMouseDown(ev);
    }
    activateSheet() {
        this.env.model.dispatch("ACTIVATE_SHEET", {
            sheetIdFrom: this.env.model.getters.getActiveSheetId(),
            sheetIdTo: this.props.sheetId,
        });
        this.scrollToSheet();
    }
    onDblClick() {
        if (this.env.model.getters.isReadonly()) {
            return;
        }
        this.startEdition();
    }
    onKeyDown(ev) {
        if (!this.state.isEditing)
            return;
        if (ev.key === "Enter") {
            ev.preventDefault();
            this.stopEdition();
            this.DOMFocusableElementStore.focus();
        }
        if (ev.key === "Escape") {
            this.cancelEdition();
            this.DOMFocusableElementStore.focus();
        }
    }
    onMouseEventSheetName(ev) {
        if (this.state.isEditing)
            ev.stopPropagation();
    }
    startEdition() {
        this.state.isEditing = true;
        this.editionState = "initializing";
    }
    stopEdition() {
        if (!this.state.isEditing || !this.sheetNameRef.el)
            return;
        this.state.isEditing = false;
        this.editionState = "initializing";
        this.sheetNameRef.el.blur();
        const inputValue = this.getInputContent() || "";
        interactiveRenameSheet(this.env, this.props.sheetId, inputValue, () => this.startEdition());
    }
    cancelEdition() {
        this.state.isEditing = false;
        this.editionState = "initializing";
        this.sheetNameRef.el?.blur();
        this.setInputContent(this.sheetName);
    }
    onIconClick(ev) {
        if (!this.isSheetActive) {
            this.activateSheet();
        }
        this.props.openContextMenu(this.contextMenuRegistry, ev);
    }
    onContextMenu(ev) {
        if (!this.isSheetActive) {
            this.activateSheet();
        }
        this.props.openContextMenu(this.contextMenuRegistry, ev);
    }
    getInputContent() {
        return this.sheetNameRef.el?.textContent;
    }
    setInputContent(content) {
        if (this.sheetNameRef.el)
            this.sheetNameRef.el.textContent = content;
    }
    onColorPicked(color) {
        this.state.pickerOpened = false;
        this.env.model.dispatch("COLOR_SHEET", { sheetId: this.props.sheetId, color });
    }
    get colorPickerAnchorRect() {
        const button = this.sheetDivRef.el;
        return getBoundingRectAsPOJO(button);
    }
    get contextMenuRegistry() {
        return getSheetMenuRegistry({
            renameSheetCallback: () => {
                this.scrollToSheet();
                this.startEdition();
            },
            openSheetColorPickerCallback: () => {
                this.state.pickerOpened = true;
            },
        });
    }
    get isSheetActive() {
        return this.env.model.getters.getActiveSheetId() === this.props.sheetId;
    }
    get sheetName() {
        return this.env.model.getters.getSheetName(this.props.sheetId);
    }
    get sheetColorStyle() {
        const color = this.env.model.getters.getSheet(this.props.sheetId).color || "";
        return cssPropertiesToCss({ background: color });
    }
}

const selectionStatisticFunctions = [
    {
        name: _t("Sum"),
        types: [CellValueType.number],
        compute: (values, locale) => sum([[values]], locale),
    },
    {
        name: _t("Avg"),
        types: [CellValueType.number],
        compute: (values, locale) => average([[values]], locale),
    },
    {
        name: _t("Min"),
        types: [CellValueType.number],
        compute: (values, locale) => min([[values]], locale).value,
    },
    {
        name: _t("Max"),
        types: [CellValueType.number],
        compute: (values, locale) => max([[values]], locale).value,
    },
    {
        name: _t("Count"),
        types: [CellValueType.number, CellValueType.text, CellValueType.boolean, CellValueType.error],
        compute: (values) => countAny([[values]]),
    },
    {
        name: _t("Count Numbers"),
        types: [CellValueType.number, CellValueType.text, CellValueType.boolean, CellValueType.error],
        compute: (values, locale) => countNumbers([[values]], locale),
    },
];
class AggregateStatisticsStore extends SpreadsheetStore {
    statisticFnResults = this._computeStatisticFnResults();
    isDirty = false;
    constructor(get) {
        super(get);
        this.model.selection.observe(this, {
            handleEvent: this.handleEvent.bind(this),
        });
    }
    handle(cmd) {
        if (invalidateEvaluationCommands.has(cmd.type) ||
            (cmd.type === "UPDATE_CELL" && ("content" in cmd || "format" in cmd))) {
            this.isDirty = true;
        }
        switch (cmd.type) {
            case "HIDE_COLUMNS_ROWS":
            case "UNHIDE_COLUMNS_ROWS":
            case "GROUP_HEADERS":
            case "UNGROUP_HEADERS":
            case "ACTIVATE_SHEET":
            case "ACTIVATE_NEXT_SHEET":
            case "ACTIVATE_PREVIOUS_SHEET":
            case "EVALUATE_CELLS":
            case "UNDO":
            case "REDO":
                this.isDirty = true;
        }
    }
    finalize() {
        if (this.isDirty) {
            this.isDirty = false;
            this.statisticFnResults = this._computeStatisticFnResults();
        }
    }
    handleEvent() {
        if (this.getters.isGridSelectionActive()) {
            this.statisticFnResults = this._computeStatisticFnResults();
        }
    }
    _computeStatisticFnResults() {
        const getters = this.getters;
        const sheetId = getters.getActiveSheetId();
        const cells = [];
        const recomputedZones = recomputeZones(getters.getSelectedZones(), []);
        const heightMax = this.getters.getSheetSize(sheetId).numberOfRows - 1;
        const widthMax = this.getters.getSheetSize(sheetId).numberOfCols - 1;
        for (const zone of recomputedZones) {
            for (let col = zone.left; col <= (zone.right ?? widthMax); col++) {
                for (let row = zone.top; row <= (zone.bottom ?? heightMax); row++) {
                    if (getters.isRowHidden(sheetId, row) || getters.isColHidden(sheetId, col)) {
                        continue; // Skip hidden cells
                    }
                    const evaluatedCell = getters.getEvaluatedCell({ sheetId, col, row });
                    if (evaluatedCell.type !== CellValueType.empty) {
                        cells.push(evaluatedCell);
                    }
                }
            }
        }
        const locale = getters.getLocale();
        const statisticFnResults = {};
        const getCells = memoize((typeStr) => {
            const types = typeStr.split(",");
            return cells.filter((c) => types.includes(c.type));
        });
        for (const 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;
            const evaluatedCells = getCells(fn.types.sort().join(","));
            if (evaluatedCells.length) {
                fnResult = lazy(() => fn.compute(evaluatedCells, locale));
            }
            statisticFnResults[fn.name] = fnResult;
        }
        return statisticFnResults;
    }
}

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

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

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

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

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

      .o-sheet-list {
        overflow-y: hidden;
        overflow-x: auto;

        &::-webkit-scrollbar {
          display: none; /* Chrome */
        }
        -ms-overflow-style: none; /* IE and Edge */
        scrollbar-width: none; /* Firefox */
      }
    }

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

        .o-icon {
          height: 18px;
          width: 18px;
          font-size: 18px;
        }
      }
    }
  }

  .o-spreadsheet-mobile .o-spreadsheet-bottom-bar {
    padding-left: 0;

    .add-sheet-container {
      order: 2;
    }

    .o-add-sheet {
      margin-left: 0.5rem;
    }

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

class ClickableCellsStore extends SpreadsheetStore {
    _clickableCells = markRaw({});
    _registryItems = markRaw(clickableCellRegistry.getAll().sort((a, b) => a.sequence - b.sequence));
    handle(cmd) {
        if (invalidateEvaluationCommands.has(cmd.type) ||
            cmd.type === "EVALUATE_CELLS" ||
            (cmd.type === "UPDATE_CELL" && ("content" in cmd || "format" in cmd))) {
            this._clickableCells = markRaw({});
            this._registryItems = markRaw(clickableCellRegistry.getAll().sort((a, b) => a.sequence - b.sequence));
        }
    }
    getClickableItem(position) {
        const { sheetId, col, row } = position;
        const clickableCells = this._clickableCells;
        const xc = toXC(col, row);
        if (!clickableCells[sheetId]) {
            clickableCells[sheetId] = {};
        }
        if (!(xc in clickableCells[sheetId])) {
            const clickableCell = this.findClickableItem(position);
            clickableCells[sheetId][xc] = clickableCell;
        }
        return clickableCells[sheetId][xc];
    }
    findClickableItem(position) {
        const getters = this.getters;
        for (const item of this._registryItems) {
            if (item.condition(position, getters)) {
                return item;
            }
        }
        return undefined;
    }
    get clickableCells() {
        const cells = [];
        const getters = this.getters;
        for (const position of this.getters.getVisibleCellPositions()) {
            const item = this.getClickableItem(position);
            if (!item) {
                continue;
            }
            const title = typeof item.title === "function" ? item.title(position, getters) : item.title;
            const rect = this.getClickableCellRect(position);
            if (!rect) {
                continue;
            }
            cells.push({
                coordinates: rect,
                position,
                action: item.execute,
                title: title || "",
                component: item.component,
                componentProps: item.componentProps?.(position, getters) ?? {},
            });
        }
        return cells;
    }
    getClickableCellRect(position) {
        const zone = this.getters.expandZone(position.sheetId, positionToZone(position));
        const clickableRect = this.getters.getVisibleRect(zone);
        const icons = this.getters.getCellIcons(position);
        const iconsAtPosition = {
            center: icons.find((icon) => icon.horizontalAlign === "center"),
            left: icons.find((icon) => icon.horizontalAlign === "left"),
            right: icons.find((icon) => icon.horizontalAlign === "right"),
        };
        if (iconsAtPosition.center?.onClick) {
            return undefined;
        }
        if (iconsAtPosition.right?.onClick) {
            const cellRect = this.getters.getRect(zone);
            const iconRect = this.getters.getCellIconRect(iconsAtPosition.right, cellRect);
            clickableRect.width -= iconRect.width + iconsAtPosition.right.margin;
        }
        if (iconsAtPosition.left?.onClick) {
            const cellRect = this.getters.getRect(zone);
            const iconRect = this.getters.getCellIconRect(iconsAtPosition.left, cellRect);
            clickableRect.x += iconRect.width + iconsAtPosition.left.margin;
            clickableRect.width -= iconRect.width + iconsAtPosition.left.margin;
        }
        return clickableRect;
    }
}

css /* scss */ `
  .o-dashboard-clickable-cell {
    position: absolute;
    cursor: pointer;
  }
`;
class SpreadsheetDashboard extends Component {
    static template = "o-spreadsheet-SpreadsheetDashboard";
    static props = { getGridSize: Function };
    static components = {
        GridOverlay,
        GridPopover,
        Popover,
        VerticalScrollBar,
        HorizontalScrollBar,
    };
    cellPopovers;
    onMouseWheel;
    canvasPosition;
    hoveredCell;
    clickableCellsStore;
    gridRef;
    setup() {
        this.gridRef = useRef("grid");
        this.hoveredCell = useStore(DelayedHoveredCellStore);
        this.clickableCellsStore = useStore(ClickableCellsStore);
        useChildSubEnv({ getPopoverContainerRect: () => this.getGridRect() });
        useGridDrawing("canvas", this.env.model, () => this.env.model.getters.getSheetViewDimension());
        this.onMouseWheel = useWheelHandler((deltaX, deltaY) => {
            this.moveCanvas(deltaX, deltaY);
            this.hoveredCell.clear();
        });
        this.cellPopovers = useStore(CellPopoverStore);
        useTouchScroll(this.gridRef, this.moveCanvas.bind(this), () => {
            const { scrollY } = this.env.model.getters.getActiveSheetScrollInfo();
            return scrollY > 0;
        }, () => {
            const { maxOffsetY } = this.env.model.getters.getMaximumSheetOffset();
            const { scrollY } = this.env.model.getters.getActiveSheetScrollInfo();
            return scrollY < maxOffsetY;
        });
    }
    get gridContainer() {
        const maxWidth = this.getMaxSheetWidth();
        return cssPropertiesToCss({ "max-width": `${maxWidth}px` });
    }
    get gridOverlayDimensions() {
        return cssPropertiesToCss({
            height: "100%",
            width: "100%",
        });
    }
    getCellClickableStyle(coordinates) {
        return cssPropertiesToCss({
            top: `${coordinates.y}px`,
            left: `${coordinates.x}px`,
            width: `${coordinates.width}px`,
            height: `${coordinates.height}px`,
        });
    }
    /**
     * Get all the boxes for the cell in the sheet view that are clickable.
     * This function is used to render an overlay over each clickable cell in
     * order to display a pointer cursor.
     *
     */
    getClickableCells() {
        return toRaw(this.clickableCellsStore.clickableCells);
    }
    selectClickableCell(ev, clickableCell) {
        const { position, action } = clickableCell;
        action(position, this.env, isMiddleClickOrCtrlClick(ev));
    }
    onClosePopover() {
        this.cellPopovers.close();
    }
    onGridResized() {
        const { height, width } = this.props.getGridSize();
        const maxWidth = this.getMaxSheetWidth();
        this.env.model.dispatch("RESIZE_SHEETVIEW", {
            width: Math.min(maxWidth, width),
            height,
            gridOffsetX: 0,
            gridOffsetY: 0,
        });
    }
    moveCanvas(deltaX, deltaY) {
        const { scrollX, scrollY } = this.env.model.getters.getActiveSheetScrollInfo();
        this.env.model.dispatch("SET_VIEWPORT_OFFSET", {
            offsetX: scrollX + deltaX,
            offsetY: scrollY + deltaY,
        });
    }
    getGridRect() {
        return {
            ...getRefBoundingRect(this.gridRef),
            ...this.env.model.getters.getSheetViewDimensionWithHeaders(),
        };
    }
    getMaxSheetWidth() {
        const sheetId = this.env.model.getters.getActiveSheetId();
        const { right } = this.env.model.getters.getSheetZone(sheetId);
        return this.env.model.getters.getColDimensions(sheetId, right).end;
    }
}

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

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

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

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

function useScreenWidth() {
    const spreadsheetRect = useSpreadsheetRect();
    return {
        get isSmall() {
            return spreadsheetRect.width < MOBILE_WIDTH_BREAKPOINT;
        },
    };
}

css /* scss */ `
  .o-sidePanel {
    display: flex;
    flex-direction: column;
    overflow-x: hidden;
    background-color: white;
    border: solid ${GRAY_300};
    border-width: 1px 0 0 1px;
    user-select: none;
    color: ${TEXT_BODY};

    &.collapsed {
      padding: 8px;
      cursor: pointer;

      .o-sidePanelTitle {
        writing-mode: vertical-rl;
        text-orientation: mixed;
      }
    }

    .o-sidePanelTitle {
      line-height: 20px;
      font-size: 16px;
    }

    .o-sidePanelHeader {
      padding: 8px;
      border-bottom: 1px solid ${GRAY_300};
    }

    .o-sidePanelAction {
      padding: 5px 10px;
      cursor: pointer;

      &.active {
        background-color: ${BUTTON_ACTIVE_BG};
      }

      &:hover {
        background-color: ${BUTTON_HOVER_BG};
      }
    }
    .o-sidePanelBody-container {
      /* This overwrites the min-height: auto; of flex. Without this, a flex div cannot be smaller than its children */
      min-height: 0;
    }
    .o-sidePanelBody {
      overflow: auto;
      width: 100%;
      height: 100%;

      .o-section {
        padding: 16px;

        .o-section-title {
          font-weight: 500;
          margin-bottom: 5px;
        }

        .o-section-subtitle {
          font-weight: 500;
          font-size: 13px;
          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-composer {
        color: ${TEXT_BODY};
      }
    }

    .o-sidePanelButtons {
      display: flex;
      gap: 8px;
    }

    .o-invalid {
      border-width: 2px;
      border-color: red;
    }

    .o-sidePanel-handle-container {
      width: 8px;
      position: fixed;
      top: 50%;
      z-index: 1;
    }
    .o-sidePanel-handle {
      cursor: col-resize;
      color: #a9a9a9;
      .o-icon {
        height: 25px;
        margin-left: -5px;
      }
    }
  }

  .o-fw-bold {
    font-weight: 500;
  }
`;
class SidePanel extends Component {
    static template = "o-spreadsheet-SidePanel";
    static props = {
        panelContent: Object,
        panelProps: Object,
        onCloseSidePanel: Function,
        onStartHandleDrag: Function,
        onResetPanelSize: Function,
        isPinned: { type: Boolean, optional: true },
        onTogglePinPanel: { type: Function, optional: true },
        onToggleCollapsePanel: { type: Function, optional: true },
        isCollapsed: { type: Boolean, optional: true },
    };
    spreadsheetRect = useSpreadsheetRect();
    getTitle() {
        const panel = this.props.panelContent;
        return typeof panel.title === "function"
            ? panel.title(this.env, this.props.panelProps)
            : panel.title;
    }
    get pinInfoMessage() {
        return _t("Pin this panel to allow to open another side panel beside it.");
    }
}

class SidePanels extends Component {
    static template = "o-spreadsheet-SidePanels";
    static props = {};
    static components = { SidePanel };
    sidePanelStore;
    spreadsheetRect = useSpreadsheetRect();
    setup() {
        this.sidePanelStore = useStore(SidePanelStore);
        useEffect(() => {
            if (this.sidePanelStore.mainPanel && !this.sidePanelStore.isMainPanelOpen) {
                this.sidePanelStore.closeMainPanel();
            }
            if (this.sidePanelStore.secondaryPanel && !this.sidePanelStore.isSecondaryPanelOpen) {
                this.sidePanelStore.close();
            }
        }, () => [this.sidePanelStore.isMainPanelOpen, this.sidePanelStore.isSecondaryPanelOpen]);
    }
    startHandleDrag(panel, ev) {
        const startingCursor = document.body.style.cursor;
        const panelInfo = panel === "mainPanel" ? this.sidePanelStore.mainPanel : this.sidePanelStore.secondaryPanel;
        if (!panelInfo) {
            return;
        }
        const startSize = panelInfo.size;
        const startPosition = ev.clientX;
        const onMouseMove = (ev) => {
            document.body.style.cursor = "col-resize";
            const newSize = startSize + startPosition - ev.clientX;
            this.sidePanelStore.changePanelSize(panel, newSize);
        };
        const cleanUp = () => {
            document.body.style.cursor = startingCursor;
        };
        startDnd(onMouseMove, cleanUp);
    }
    get mainPanelProps() {
        const panelProps = this.sidePanelStore.mainPanelProps;
        if (!this.sidePanelStore.mainPanel || !panelProps) {
            return undefined;
        }
        return {
            panelContent: sidePanelRegistry.get(this.sidePanelStore.mainPanel.componentTag),
            panelProps,
            onCloseSidePanel: () => this.sidePanelStore.closeMainPanel(),
            onTogglePinPanel: () => this.sidePanelStore.togglePinPanel(),
            onStartHandleDrag: (ev) => this.startHandleDrag("mainPanel", ev),
            onResetPanelSize: () => this.sidePanelStore.resetPanelSize("mainPanel"),
            isPinned: this.sidePanelStore.mainPanel?.isPinned,
            onToggleCollapsePanel: () => this.sidePanelStore.toggleCollapsePanel("mainPanel"),
            isCollapsed: this.sidePanelStore.mainPanel?.isCollapsed,
        };
    }
    get secondaryPanelProps() {
        const panelProps = this.sidePanelStore.secondaryPanelProps;
        if (!this.sidePanelStore.secondaryPanel || !panelProps) {
            return undefined;
        }
        return {
            panelContent: sidePanelRegistry.get(this.sidePanelStore.secondaryPanel.componentTag),
            panelProps,
            onCloseSidePanel: () => this.sidePanelStore.close(),
            onStartHandleDrag: (ev) => this.startHandleDrag("secondaryPanel", ev),
            onResetPanelSize: () => this.sidePanelStore.resetPanelSize("secondaryPanel"),
            onToggleCollapsePanel: () => this.sidePanelStore.toggleCollapsePanel("secondaryPanel"),
            isCollapsed: this.sidePanelStore.secondaryPanel?.isCollapsed,
        };
    }
    get panelList() {
        return [
            {
                key: this.sidePanelStore.secondaryPanelKey,
                props: this.secondaryPanelProps,
                style: this.sidePanelStore.secondaryPanel
                    ? cssPropertiesToCss({ width: `${this.sidePanelStore.secondaryPanel.size}px` })
                    : "",
            },
            {
                key: this.sidePanelStore.mainPanelKey,
                props: this.mainPanelProps,
                style: this.sidePanelStore.mainPanel
                    ? cssPropertiesToCss({ width: `${this.sidePanelStore.mainPanel.size}px` })
                    : "",
            },
        ].filter((panel) => panel.key && panel.props);
    }
}

class RibbonMenu extends Component {
    static template = "o-spreadsheet-RibbonMenu";
    static props = {
        onClose: Function,
    };
    static components = { Menu };
    rootItems = topbarMenuRegistry.getMenuItems();
    menuRef = useRef("menu");
    containerRef = useRef("container");
    state = useState({
        menuItems: this.rootItems,
        title: _t("Menu Bar"),
        parentState: undefined,
    });
    setup() {
        useExternalListener(window, "click", this.onExternalClick, { capture: true });
        onMounted(this.updateShadows);
    }
    onExternalClick(ev) {
        if (!this.menuRef.el?.contains(ev.target)) {
            this.props.onClose();
        }
    }
    onClickMenu(menu) {
        const children = menu.children(this.env);
        if (children.length) {
            this.state.parentState = { ...this.state };
            this.state.menuItems = children;
            this.state.title = menu.name(this.env);
            this.containerRef.el?.scrollTo({ top: 0 });
        }
        else {
            this.state.menuItems = this.rootItems;
            this.state.title = undefined;
            this.state.parentState = undefined;
            menu.execute?.(this.env);
            this.props.onClose();
        }
    }
    get menuProps() {
        return {
            menuItems: getMenuItemsAndSeparators(this.env, this.state.menuItems),
            onClose: this.props.onClose,
            onClickMenu: this.onClickMenu.bind(this),
        };
    }
    get style() {
        return cssPropertiesToCss({
            height: `${this.props.height}px`,
        });
    }
    updateShadows() {
        if (!this.containerRef.el) {
            return;
        }
        this.containerRef.el.classList.remove("scroll-top", "scroll-bottom");
        const maxScroll = this.containerRef.el.scrollHeight - this.containerRef.el.clientHeight || 0;
        if (this.containerRef.el.scrollTop < maxScroll - 1) {
            this.containerRef.el.classList.add("scroll-bottom");
        }
        if (this.containerRef.el.scrollTop > 0) {
            this.containerRef.el.classList.add("scroll-top");
        }
    }
    onClickBack() {
        if (!this.state.parentState) {
            this.props.onClose();
            return;
        }
        this.state.menuItems = this.state.parentState.menuItems;
        this.state.title = this.state.parentState.title;
        this.state.parentState = this.state.parentState.parentState;
        this.containerRef.el?.scrollTo({ top: 0 });
    }
    get backTitle() {
        return this.state.parentState ? _t("Go to previous menu") : _t("Close menu bar");
    }
}

css `
  .o-small-composer {
    z-index: ${ComponentsImportance.TopBarComposer};
  }
`;
class SmallBottomBar extends Component {
    static components = { Composer, BottomBar, Ripple, RibbonMenu };
    static template = "o-spreadsheet-SmallBottomBar";
    static props = {
        onClick: Function,
    };
    composerFocusStore;
    composerStore;
    composerInterface;
    composerRef = useRef("bottombarComposer");
    menuState = useState({
        isOpen: false,
    });
    setup() {
        this.composerFocusStore = useStore(ComposerFocusStore);
        const composerStore = useStore(CellComposerStore);
        this.composerStore = composerStore;
        this.composerInterface = {
            id: "bottombarComposer",
            get editionMode() {
                return composerStore.editionMode;
            },
            startEdition: this.composerStore.startEdition,
            setCurrentContent: this.composerStore.setCurrentContent,
            stopEdition: this.composerStore.stopEdition,
        };
        useEffect(() => {
            if (
            // we hide the grid composer on mobile so we need to autofocus this composer
            this.env.isMobile() &&
                !this.menuState.isOpen &&
                this.composerStore.editionMode !== "inactive" &&
                this.composerFocusStore.activeComposer !== this.composerInterface) {
                this.composerFocusStore.focusComposer(this.composerInterface, {
                    focusMode: "contentFocus",
                });
            }
        });
    }
    get focus() {
        return this.composerFocusStore.activeComposer === this.composerInterface
            ? this.composerFocusStore.focusMode
            : "inactive";
    }
    get showFxIcon() {
        return (this.focus === "inactive" &&
            !this.composerStore.currentContent &&
            !this.composerStore.placeholder);
    }
    get rect() {
        return this.composerRef.el
            ? getBoundingRectAsPOJO(this.composerRef.el)
            : { x: 0, y: 0, width: 0, height: 0 };
    }
    get composerProps() {
        const { width, height } = this.env.model.getters.getSheetViewDimensionWithHeaders();
        return {
            rect: { ...this.rect },
            delimitation: {
                width,
                height,
            },
            focus: this.focus,
            composerStore: this.composerStore,
            onComposerContentFocused: (selection) => this.composerFocusStore.focusComposer(this.composerInterface, {
                focusMode: "contentFocus",
                selection,
            }),
            isDefaultFocus: false,
            inputStyle: cssPropertiesToCss({
                height: this.focus === "inactive" ? "26px" : "fit-content",
                "max-height": `130px`,
            }),
            showAssistant: false, // Hide assistant in small composer as it gets cropped ATM
            placeholder: this.composerStore.placeholder,
        };
    }
    get symbols() {
        return ["=", "(", ")", ":", "-", "/", "*", ",", "+", "$", "."];
    }
    insertSymbol(symbol) {
        this.composerStore.replaceComposerCursorSelection(symbol);
        this.composerFocusStore.focusComposer(this.composerInterface, {
            focusMode: "contentFocus",
        });
    }
    toggleRibbon() {
        this.composerStore.cancelEdition();
        this.menuState.isOpen = !this.menuState.isOpen;
    }
}

const COMPOSER_MAX_HEIGHT = 300;
css /* scss */ `
  .o-topbar-composer-container {
    height: ${DESKTOP_TOPBAR_TOOLBAR_HEIGHT}px;
  }

  .o-topbar-composer {
    height: fit-content;
    margin-top: -1px;
    margin-bottom: -1px;
    border: 1px solid;
    font-family: ${DEFAULT_FONT};
  }

  .user-select-text {
    user-select: text;
  }
`;
class TopBarComposer extends Component {
    static template = "o-spreadsheet-TopBarComposer";
    static props = {};
    static components = { Composer };
    composerFocusStore;
    composerStore;
    composerInterface;
    setup() {
        this.composerFocusStore = useStore(ComposerFocusStore);
        const composerStore = useStore(CellComposerStore);
        this.composerStore = composerStore;
        this.composerInterface = {
            id: "topbarComposer",
            get editionMode() {
                return composerStore.editionMode;
            },
            startEdition: this.composerStore.startEdition,
            setCurrentContent: this.composerStore.setCurrentContent,
            stopEdition: this.composerStore.stopEdition,
        };
    }
    get focus() {
        return this.composerFocusStore.activeComposer === this.composerInterface
            ? this.composerFocusStore.focusMode
            : "inactive";
    }
    get showFxIcon() {
        return (this.focus === "inactive" &&
            !this.composerStore.currentContent &&
            !this.composerStore.placeholder);
    }
    get composerStyle() {
        const style = {
            padding: "5px 0px 5px 8px",
            "max-height": `${COMPOSER_MAX_HEIGHT}px`,
            "line-height": "24px",
        };
        style.height = this.focus === "inactive" ? `${DESKTOP_TOPBAR_TOOLBAR_HEIGHT}px` : "fit-content";
        return cssPropertiesToCss(style);
    }
    get containerStyle() {
        if (this.focus === "inactive") {
            return cssPropertiesToCss({
                "border-color": SEPARATOR_COLOR,
                "border-right": "none",
            });
        }
        return cssPropertiesToCss({
            "border-color": SELECTION_BORDER_COLOR,
            "z-index": String(ComponentsImportance.TopBarComposer),
        });
    }
    onFocus(selection) {
        this.composerFocusStore.focusComposer(this.composerInterface, { selection });
    }
}

/**
 * This store is used to manage the dropdown that is currently
 * opened after clicking an item on the toolbar.
 * It can only have one displayed at a time.
 *
 */
class TopBarToolStore {
    mutators = ["closeDropdowns", "openDropdown"];
    _currentDropdown = null;
    closeDropdowns() {
        this._currentDropdown = null;
    }
    openDropdown(dropdownComponent) {
        this._currentDropdown = dropdownComponent;
    }
    get currentDropdown() {
        return this._currentDropdown;
    }
}

class ToolBarRegistry {
    content = {};
    add(key) {
        if (key in this.content) {
            throw new Error(`${key} is already present in this registry!`);
        }
        this.content[key] = [];
        return this;
    }
    addChild(key, value) {
        this.content[key].push(value);
        return this;
    }
    getEntries(id) {
        return this.content[id].sort((a, b) => a.sequence - b.sequence);
    }
    getCategories() {
        return Object.keys(this.content);
    }
}

function useToolBarDropdownStore() {
    const component = useComponent();
    const topbarStore = useStore(TopBarToolStore);
    onWillUnmount(() => {
        if (component === topbarStore.currentDropdown) {
            topbarStore.closeDropdowns();
        }
    });
    return {
        closeDropdowns: () => topbarStore.closeDropdowns(),
        openDropdown: () => {
            topbarStore.openDropdown(component);
        },
        get isActive() {
            return topbarStore.currentDropdown === component;
        },
    };
}

/**
 * List the available borders positions and the corresponding icons.
 * The structure of this array is defined to match the order/lines we want
 * to display in the topbar's border tool.
 */
const BORDER_POSITIONS = [
    [
        ["all", "o-spreadsheet-Icon.BORDERS"],
        ["hv", "o-spreadsheet-Icon.BORDER_HV"],
        ["h", "o-spreadsheet-Icon.BORDER_H"],
        ["v", "o-spreadsheet-Icon.BORDER_V"],
        ["external", "o-spreadsheet-Icon.BORDER_EXTERNAL"],
    ],
    [
        ["left", "o-spreadsheet-Icon.BORDER_LEFT"],
        ["top", "o-spreadsheet-Icon.BORDER_TOP"],
        ["right", "o-spreadsheet-Icon.BORDER_RIGHT"],
        ["bottom", "o-spreadsheet-Icon.BORDER_BOTTOM"],
        ["clear", "o-spreadsheet-Icon.BORDER_CLEAR"],
    ],
];
// -----------------------------------------------------------------------------
// Border Editor
// -----------------------------------------------------------------------------
css /* scss */ `
  .o-border-selector {
    padding: 4px;
    background-color: white;

    .o-divider {
      border-right: 1px solid ${GRAY_300};
      margin: 0 6px;
    }

    .o-border-selector-section {
      .o-dropdown-line {
        height: 30px;
        margin: 1px;
        .o-line-item {
          padding: 4px;
          width: 26px;
          height: 26px;
          &.active {
            background-color: ${BUTTON_ACTIVE_BG};
          }
        }
      }
      .o-border-style-tool {
        padding: 0px 3px;
        margin: 2px;
        height: 25px;
      }
    }
  }

  .o-border-style-dropdown {
    background: #ffffff;
    padding: 4px;
    .o-dropdown-line {
    }
    .o-style-preview {
      margin: 7px 5px 7px 5px;
      width: 60px;
      height: 5px;
    }
    .o-style-thin {
      border-bottom: 1px solid #000000;
    }
    .o-style-medium {
      border-bottom: 2px solid #000000;
    }
    .o-style-thick {
      border-bottom: 3px solid #000000;
    }
    .o-style-dashed {
      border-bottom: 1px dashed #000000;
    }
    .o-style-dotted {
      border-bottom: 1px dotted #000000;
    }
    .o-dropdown-border-type {
      cursor: pointer;
      &:not(.o-disabled):not(.active):hover {
        background-color: ${BUTTON_HOVER_BG};
      }
    }
    .o-dropdown-border-check {
      width: 20px;
      font-size: 12px;
    }
    .o-border-picker-button {
      padding: 0px !important;
      margin: 5px 0px 0px 0px !important;
      height: 25px !important;
    }
  }
`;
class BorderEditor extends Component {
    static template = "o-spreadsheet-BorderEditor";
    static props = {
        class: { type: String, optional: true },
        currentBorderColor: { type: String, optional: false },
        currentBorderStyle: { type: String, optional: false },
        currentBorderPosition: { type: String, optional: true },
        onBorderColorPicked: Function,
        onBorderStylePicked: Function,
        onBorderPositionPicked: Function,
        maxHeight: { type: Number, optional: true },
        anchorRect: Object,
    };
    static components = { ColorPickerWidget, Popover };
    BORDER_POSITIONS = BORDER_POSITIONS;
    lineStyleButtonRef = useRef("lineStyleButton");
    borderStyles = borderStyles;
    state = useState({
        activeTool: undefined,
    });
    toggleDropdownTool(tool) {
        const isOpen = this.state.activeTool === tool;
        this.state.activeTool = isOpen ? undefined : tool;
    }
    closeDropdown() {
        this.state.activeTool = undefined;
    }
    setBorderPosition(position) {
        this.props.onBorderPositionPicked(position);
        this.closeDropdown();
    }
    setBorderColor(color) {
        this.props.onBorderColorPicked(color);
        this.closeDropdown();
    }
    setBorderStyle(style) {
        this.props.onBorderStylePicked(style);
        this.closeDropdown();
    }
    get lineStylePickerPopoverProps() {
        return {
            anchorRect: this.lineStylePickerAnchorRect,
            positioning: "bottom-left",
            verticalOffset: 0,
        };
    }
    get popoverProps() {
        return {
            anchorRect: this.props.anchorRect,
            maxHeight: this.props.maxHeight,
            positioning: "bottom-left",
            verticalOffset: 0,
        };
    }
    get lineStylePickerAnchorRect() {
        const button = this.lineStyleButtonRef.el;
        if (button === null) {
            return { x: 0, y: 0, width: 0, height: 0 };
        }
        const buttonRect = button.getBoundingClientRect();
        return {
            x: buttonRect.x,
            y: buttonRect.y,
            width: buttonRect.width,
            height: buttonRect.height,
        };
    }
}

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

class PaintFormatButton extends Component {
    static template = "o-spreadsheet-PaintFormatButton";
    static props = {
        class: { type: String, optional: true },
    };
    paintFormatStore;
    setup() {
        this.paintFormatStore = useStore(PaintFormatStore);
    }
    get isActive() {
        return this.paintFormatStore.isActive;
    }
    onDblClick() {
        this.paintFormatStore.activate({ persistent: true });
    }
    togglePaintFormat() {
        if (this.isActive) {
            this.paintFormatStore.cancel();
        }
        else {
            this.paintFormatStore.activate({ persistent: false });
        }
    }
}

class TableDropdownButton extends Component {
    static template = "o-spreadsheet-TableDropdownButton";
    static components = { TableStylesPopover, ActionButton };
    static props = {
        class: { type: String, optional: true },
    };
    topBarToolStore;
    state = useState({ popoverProps: undefined });
    setup() {
        this.topBarToolStore = useToolBarDropdownStore();
    }
    onStylePicked(styleId) {
        const sheetId = this.env.model.getters.getActiveSheetId();
        const tableConfig = { ...this.tableConfig, styleId };
        const result = interactiveCreateTable(this.env, sheetId, tableConfig);
        if (result.isSuccessful) {
            this.env.openSidePanel("TableSidePanel", {});
        }
        this.closePopover();
    }
    onClick(ev) {
        if (ev.hasClosedTableStylesPopover || this.state.popoverProps) {
            this.closePopover();
            return;
        }
        if (this.env.model.getters.getFirstTableInSelection()) {
            this.topBarToolStore.closeDropdowns();
            this.env.toggleSidePanel("TableSidePanel", {});
            return;
        }
        const target = ev.currentTarget;
        const { left, bottom } = target.getBoundingClientRect();
        this.topBarToolStore.openDropdown();
        this.state.popoverProps = {
            anchorRect: { x: left, y: bottom, width: 0, height: 0 },
            positioning: "bottom-left",
            verticalOffset: 0,
        };
    }
    closePopover() {
        this.state.popoverProps = undefined;
    }
    get action() {
        return {
            name: (env) => env.model.getters.getFirstTableInSelection() ? _t("Edit table") : _t("Insert table"),
            icon: (env) => env.model.getters.getFirstTableInSelection()
                ? "o-spreadsheet-Icon.EDIT_TABLE"
                : "o-spreadsheet-Icon.PAINT_TABLE",
        };
    }
    get tableConfig() {
        return { ...DEFAULT_TABLE_CONFIG, numberOfHeaders: 1, bandedRows: true };
    }
}

class TopBarColorEditor extends Component {
    static components = { ColorPickerWidget };
    static props = { class: String, style: String, icon: String, title: String };
    static template = "o-spreadsheet-ColorEditor";
    topBarToolStore;
    state = useState({
        isOpen: false,
    });
    setup() {
        this.topBarToolStore = useToolBarDropdownStore();
    }
    get currentColor() {
        return (this.env.model.getters.getCurrentStyle()[this.props.style] ||
            (this.props.style === "textColor" ? "#000000" : "#ffffff"));
    }
    setColor(color) {
        setStyle(this.env, { [this.props.style]: color });
        this.state.isOpen = false;
    }
    get isMenuOpen() {
        return this.topBarToolStore.isActive;
    }
    onClick() {
        if (!this.isMenuOpen) {
            this.topBarToolStore.openDropdown();
        }
        else {
            this.topBarToolStore.closeDropdowns();
        }
    }
}

class DropdownAction extends Component {
    static template = "o-spreadsheet-DropdownAction";
    static components = { ActionButton, Popover };
    static props = {
        parentAction: Object,
        childActions: Array,
        class: String,
        childClass: String,
    };
    topBarToolStore;
    actionRef = useRef("actionRef");
    setup() {
        this.topBarToolStore = useToolBarDropdownStore();
    }
    toggleDropdown() {
        if (this.isActive) {
            this.topBarToolStore.closeDropdowns();
        }
        else {
            this.topBarToolStore.openDropdown();
        }
    }
    get isActive() {
        return this.topBarToolStore.isActive;
    }
    get popoverProps() {
        const rect = this.actionRef.el
            ? this.actionRef.el.getBoundingClientRect()
            : { x: 0, y: 0, width: 0, height: 0 };
        return {
            anchorRect: rect,
            positioning: "bottom-left",
            verticalOffset: 0,
            class: "rounded",
        };
    }
}

class TopBarFontSizeEditor extends Component {
    static components = { FontSizeEditor };
    static props = { class: String };
    static template = "o-spreadsheet-TopBarFontSizeEditor";
    topBarToolStore;
    setup() {
        this.topBarToolStore = useToolBarDropdownStore();
    }
    get currentFontSize() {
        return this.env.model.getters.getCurrentStyle().fontSize || DEFAULT_FONT_SIZE;
    }
    setFontSize(fontSize) {
        setStyle(this.env, { fontSize });
    }
    onToggle() {
        if (this.isActive) {
            this.topBarToolStore.closeDropdowns();
        }
        else {
            this.topBarToolStore.openDropdown();
        }
    }
    onFocusInput() {
        this.topBarToolStore.openDropdown();
    }
    get isActive() {
        return this.topBarToolStore.isActive;
    }
}

class NumberFormatsTool extends Component {
    static template = "o-spreadsheet-NumberFormatsTool";
    static components = { MenuPopover, ActionButton };
    static props = { class: String };
    formatNumberMenuItemSpec = formatNumberMenuItemSpec;
    topBarToolStore;
    buttonRef = useRef("buttonRef");
    state = useState({
        anchorRect: { x: 0, y: 0, width: 0, height: 0 },
        menuItems: [],
    });
    setup() {
        this.topBarToolStore = useToolBarDropdownStore();
    }
    toggleMenu() {
        if (this.isActive) {
            this.topBarToolStore.closeDropdowns();
        }
        else {
            const menu = createAction(this.formatNumberMenuItemSpec);
            this.state.menuItems = menu.children(this.env).sort((a, b) => a.sequence - b.sequence);
            this.state.anchorRect = getBoundingRectAsPOJO(this.buttonRef.el);
            this.topBarToolStore.openDropdown();
        }
    }
    get isActive() {
        return this.topBarToolStore.isActive;
    }
}

const topBarToolBarRegistry = new ToolBarRegistry();
topBarToolBarRegistry
    .add("edit")
    .addChild("edit", {
    component: ActionButton,
    props: {
        action: undo,
        class: "o-hoverable-button o-toolbar-button",
    },
    sequence: 1,
})
    .addChild("edit", {
    component: ActionButton,
    props: {
        action: redo,
        class: "o-hoverable-button o-toolbar-button",
    },
    sequence: 2,
})
    .addChild("edit", {
    component: PaintFormatButton,
    props: {
        class: "o-hoverable-button o-toolbar-button o-mobile-disabled",
    },
    sequence: 3,
})
    .addChild("edit", {
    component: ActionButton,
    props: {
        action: clearFormat,
        class: "o-hoverable-button o-toolbar-button",
    },
    sequence: 4,
})
    .add("numberFormat")
    .addChild("numberFormat", {
    component: ActionButton,
    props: {
        action: formatPercent,
        class: "o-hoverable-button o-toolbar-button",
    },
    sequence: 1,
})
    .addChild("numberFormat", {
    component: ActionButton,
    props: {
        action: decreaseDecimalPlaces,
        class: "o-hoverable-button o-toolbar-button",
    },
    sequence: 2,
})
    .addChild("numberFormat", {
    component: ActionButton,
    props: {
        action: increaseDecimalPlaces,
        class: "o-hoverable-button o-toolbar-button",
    },
    sequence: 3,
})
    .addChild("numberFormat", {
    component: NumberFormatsTool,
    props: {
        class: "o-menu-item-button o-hoverable-button o-toolbar-button",
    },
    sequence: 4,
})
    .add("fontSize")
    .addChild("fontSize", {
    component: TopBarFontSizeEditor,
    props: {
        class: "o-hoverable-button o-toolbar-button",
    },
    sequence: 3,
})
    .add("textStyle")
    .addChild("textStyle", {
    component: ActionButton,
    props: {
        action: formatBold,
        class: "o-hoverable-button o-toolbar-button",
    },
    sequence: 1,
})
    .addChild("textStyle", {
    component: ActionButton,
    props: {
        action: formatItalic,
        class: "o-hoverable-button o-toolbar-button",
    },
    sequence: 2,
})
    .addChild("textStyle", {
    component: ActionButton,
    props: {
        action: formatStrikethrough,
        class: "o-hoverable-button o-toolbar-button",
    },
    sequence: 3,
})
    .addChild("textStyle", {
    component: TopBarColorEditor,
    props: {
        class: "o-hoverable-button o-menu-item-button o-toolbar-button",
        style: "textColor",
        icon: "o-spreadsheet-Icon.TEXT_COLOR",
        title: _t("Text Color"),
    },
    sequence: 4,
})
    .add("cellStyle")
    .addChild("cellStyle", {
    component: TopBarColorEditor,
    props: {
        class: "o-hoverable-button o-menu-item-button o-toolbar-button",
        style: "fillColor",
        icon: "o-spreadsheet-Icon.FILL_COLOR",
        title: _t("Fill Color"),
    },
    sequence: 1,
})
    .addChild("cellStyle", {
    component: BorderEditorWidget,
    props: {
        class: "o-hoverable-button o-menu-item-button o-toolbar-button",
    },
    sequence: 2,
})
    .addChild("cellStyle", {
    component: ActionButton,
    props: {
        action: mergeCells,
        class: "o-hoverable-button o-menu-item-button o-toolbar-button",
    },
    sequence: 3,
})
    .add("alignment")
    .addChild("alignment", {
    component: DropdownAction,
    props: {
        parentAction: formatAlignmentHorizontal,
        childActions: [
            formatAlignmentLeft,
            formatAlignmentCenter,
            formatAlignmentRight,
        ],
        class: "o-hoverable-button o-toolbar-button",
        childClass: "o-hoverable-button",
    },
    sequence: 1,
})
    .addChild("alignment", {
    component: DropdownAction,
    props: {
        parentAction: formatAlignmentVertical,
        childActions: [
            formatAlignmentTop,
            formatAlignmentMiddle,
            formatAlignmentBottom,
        ],
        class: "o-hoverable-button o-menu-item-button o-toolbar-button",
        childClass: "o-hoverable-button",
    },
    sequence: 2,
})
    .addChild("alignment", {
    component: DropdownAction,
    props: {
        parentAction: formatWrapping,
        childActions: [
            formatWrappingOverflow,
            formatWrappingWrap,
            formatWrappingClip,
        ],
        class: "o-hoverable-button o-menu-item-button o-toolbar-button",
        childClass: "o-hoverable-button",
    },
    sequence: 3,
})
    .add("misc")
    .addChild("misc", {
    component: TableDropdownButton,
    props: { class: "o-toolbar-button o-hoverable-button o-menu-item-button o-mobile-disabled" },
    sequence: 1,
})
    .addChild("misc", {
    component: ActionButton,
    props: {
        action: createRemoveFilterTool,
        class: "o-hoverable-button o-menu-item-button o-toolbar-button",
    },
    sequence: 2,
});

// -----------------------------------------------------------------------------
// TopBar
// -----------------------------------------------------------------------------
css /* scss */ `
  .o-topbar-divider {
    border-right: 1px solid ${SEPARATOR_COLOR};
    width: 0;
    margin: 0 6px;
    height: 30px;
  }

  .o-toolbar-button {
    height: 30px;
  }

  .o-spreadsheet-topbar {
    line-height: 1.2;
    font-size: 14px;
    font-weight: 500;
    background-color: #fff;

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

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

          &.active {
            background-color: ${BUTTON_ACTIVE_BG};
            color: ${BUTTON_ACTIVE_TEXT_COLOR};
          }
        }
      }
    }

    .irregularity-map {
      border-top: 1px solid ${SEPARATOR_COLOR};
      height: ${DESKTOP_TOPBAR_TOOLBAR_HEIGHT}px;

      .alert-info {
        border-left: 3px solid ${ALERT_INFO_BORDER};
      }
    }

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

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

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

      /* Toolbar */
      .o-toolbar-tools {
        cursor: default;
      }
    }
  }

  .o-spreadsheet-mobile {
    .o-topbar-toolbar {
      height: ${MOBILE_TOPBAR_TOOLBAR_HEIGHT}px;
    }
    .o-topbar-divider {
      border-width: 2px;
      border-radius: 4px;
    }

    .o-toolbar-button {
      height: 35px;
      width: 31px;
      .o-toolbar-button.o-mobile-disabled * {
      color: ${DISABLED_TEXT_COLOR};
      cursor: not-allowed;
    }
  }
`;
class TopBar extends Component {
    static template = "o-spreadsheet-TopBar";
    static props = {
        onClick: Function,
        dropdownMaxHeight: Number,
    };
    static components = {
        MenuPopover,
        TopBarComposer,
        Popover,
    };
    toolsCategories = topBarToolBarRegistry.getCategories();
    state = useState({
        menuState: { isOpen: false, anchorRect: null, menuItems: [] },
        invisibleToolsCategories: [],
        toolsPopoverState: { isOpen: false },
    });
    isSelectingMenu = false;
    openedEl = null;
    menus = [];
    toolbarMenuRegistry = topBarToolBarRegistry;
    formatNumberMenuItemSpec = formatNumberMenuItemSpec;
    isntToolbarMenu = false;
    composerFocusStore;
    fingerprints;
    topBarToolStore;
    toolBarContainerRef = useRef("toolBarContainer");
    toolbarRef = useRef("toolBar");
    moreToolsContainerRef = useRef("moreToolsContainer");
    moreToolsButtonRef = useRef("moreToolsButton");
    spreadsheetRect = useSpreadsheetRect();
    setup() {
        this.composerFocusStore = useStore(ComposerFocusStore);
        this.fingerprints = useStore(FormulaFingerprintStore);
        this.topBarToolStore = useStore(TopBarToolStore);
        useExternalListener(window, "click", this.onExternalClick);
        onWillStart(() => this.updateCellState());
        onWillUpdateProps(() => this.updateCellState());
        useEffect(() => {
            this.state.toolsPopoverState.isOpen = false;
            this.setVisibilityToolsGroups();
        }, () => [this.spreadsheetRect.width]);
    }
    setVisibilityToolsGroups() {
        if (this.env.model.getters.isReadonly()) {
            return;
        }
        const hiddenCategories = [];
        const { x: toolsX } = this.toolbarRef.el.getBoundingClientRect();
        const { x } = this.toolBarContainerRef.el.getBoundingClientRect();
        // Compute the with of the button that will toggle the hidden tools
        this.moreToolsContainerRef.el?.classList.remove("d-none");
        const moreToolsWidth = this.moreToolsButtonRef.el?.getBoundingClientRect().width || 0;
        // The actual width in which we can place our tools so that they are visible.
        // Every tool container passed that width will be hidden.
        // We remove 16px to the width to account for a scrollbar that might appear.
        // Otherwise, we could end up in a loop of computation
        const usableWidth = Math.round(this.spreadsheetRect.width) - moreToolsWidth - (toolsX - x) - 16;
        const toolElements = document.querySelectorAll(".tool-container");
        let currentWidth = 0;
        for (let index = 0; index < toolElements.length; index++) {
            const element = toolElements[index];
            element.classList.remove("d-none");
            const { width: toolWidth } = element.getBoundingClientRect();
            currentWidth += toolWidth;
            if (currentWidth > usableWidth) {
                element.classList.add("d-none");
                hiddenCategories.push(this.toolsCategories[index]);
            }
        }
        this.state.invisibleToolsCategories = hiddenCategories;
        if (!hiddenCategories.length) {
            this.moreToolsContainerRef.el?.classList.add("d-none");
        }
    }
    get topbarComponents() {
        return topbarComponentRegistry
            .getAllOrdered()
            .filter((item) => !item.isVisible || item.isVisible(this.env));
    }
    get currentFontSize() {
        return this.env.model.getters.getCurrentStyle().fontSize || DEFAULT_FONT_SIZE;
    }
    onExternalClick(ev) {
        // TODO : manage click events better. We need this piece of code
        // otherwise the event opening the menu would close it on the same frame.
        // And we cannot stop the event propagation because it's used in an
        // external listener of the MenuPopover component to close the context menu when
        // clicking on the top bar
        if (this.openedEl === ev.target) {
            return;
        }
        this.closeMenus();
    }
    onClick() {
        this.props.onClick();
        this.closeMenus();
    }
    onMenuMouseOver(menu, ev) {
        if (this.isSelectingMenu && this.isntToolbarMenu) {
            this.openMenu(menu, ev);
        }
    }
    toggleContextMenu(menu, ev) {
        if (this.state.menuState.isOpen && this.isntToolbarMenu) {
            this.closeMenus();
        }
        else {
            this.openMenu(menu, ev);
            this.isntToolbarMenu = true;
        }
    }
    openMenu(menu, ev) {
        if (this.topBarToolStore.currentDropdown) {
            this.topBarToolStore.closeDropdowns();
        }
        this.state.toolsPopoverState.isOpen = false;
        this.state.menuState.isOpen = true;
        this.state.menuState.anchorRect = getBoundingRectAsPOJO(ev.currentTarget);
        this.state.menuState.menuItems = menu
            .children(this.env)
            .sort((a, b) => a.sequence - b.sequence);
        this.state.menuState.parentMenu = menu;
        this.isSelectingMenu = true;
        this.openedEl = ev.target;
        this.composerFocusStore.activeComposer.stopEdition();
    }
    closeMenus() {
        if (this.topBarToolStore.currentDropdown) {
            this.topBarToolStore.closeDropdowns();
        }
        this.state.toolsPopoverState.isOpen = false;
        this.state.menuState.isOpen = false;
        this.state.menuState.parentMenu = undefined;
        this.isSelectingMenu = false;
        this.openedEl = null;
    }
    updateCellState() {
        this.menus = topbarMenuRegistry.getMenuItems();
    }
    getMenuName(menu) {
        return menu.name(this.env);
    }
    setColor(target, color) {
        setStyle(this.env, { [target]: color });
        this.onClick();
    }
    setFontSize(fontSize) {
        setStyle(this.env, { fontSize });
    }
    toggleMoreTools() {
        if (this.topBarToolStore.currentDropdown) {
            this.topBarToolStore.closeDropdowns();
        }
        this.state.toolsPopoverState.isOpen = !this.state.toolsPopoverState.isOpen;
    }
    get toolsPopoverProps() {
        const rect = this.moreToolsButtonRef.el
            ? getBoundingRectAsPOJO(this.moreToolsButtonRef.el)
            : { x: 0, y: 0, width: 0, height: 0 };
        return {
            anchorRect: rect,
            positioning: "bottom-left",
            verticalOffset: 0,
            class: "rounded",
            maxWidth: 300,
        };
    }
    showDivider(categoryIndex) {
        return (categoryIndex < this.toolsCategories.length - 1 ||
            this.state.invisibleToolsCategories.length > 0);
    }
}

function instantiateClipboard() {
    return new WebClipboardWrapper(navigator.clipboard);
}
class WebClipboardWrapper {
    clipboard;
    // Can be undefined because navigator.clipboard doesn't exist in old browsers
    constructor(clipboard) {
        this.clipboard = clipboard;
    }
    async write(clipboardContent) {
        if (this.clipboard?.write) {
            try {
                await this.clipboard?.write(this.getClipboardItems(clipboardContent));
            }
            catch (e) {
                /**
                 * Some browsers do not support writing custom mimetypes in the clipboard.
                 * Therefore, we try to catch any errors and fallback on writing only standard
                 * mimetypes to prevent the whole copy action from crashing.
                 */
                console.log("Failed to write on the clipboard, falling back to plain/html text. Error %s", e);
                try {
                    await this.clipboard?.write([
                        new ClipboardItem({
                            [ClipboardMIMEType.PlainText]: this.getBlob(clipboardContent, ClipboardMIMEType.PlainText),
                            [ClipboardMIMEType.Html]: this.getBlob(clipboardContent, ClipboardMIMEType.Html),
                        }),
                    ]);
                }
                catch (e) { }
            }
        }
        else {
            await this.writeText(clipboardContent[ClipboardMIMEType.PlainText] ?? "");
        }
    }
    async writeText(text) {
        try {
            this.clipboard?.writeText(text);
        }
        catch (e) { }
    }
    async read() {
        let permissionResult = undefined;
        try {
            //@ts-ignore - clipboard-read is not implemented in all browsers
            permissionResult = await navigator.permissions.query({ name: "clipboard-read" });
        }
        catch (e) { }
        if (this.clipboard?.read) {
            try {
                const clipboardItems = await this.clipboard.read();
                const osClipboardContent = {};
                for (const item of clipboardItems) {
                    for (const type of item.types) {
                        const blob = await item.getType(type);
                        if (AllowedImageMimeTypes.includes(type)) {
                            osClipboardContent[type] = blob;
                        }
                        else {
                            const text = await blob.text();
                            osClipboardContent[type] = text;
                        }
                    }
                }
                return { status: "ok", content: osClipboardContent };
            }
            catch (e) {
                const status = permissionResult?.state === "denied" ? "permissionDenied" : "notImplemented";
                return { status };
            }
        }
        else {
            return {
                status: "ok",
                content: {
                    [ClipboardMIMEType.PlainText]: await this.clipboard?.readText(),
                },
            };
        }
    }
    getClipboardItems(content) {
        const clipboardItemData = {};
        for (const type of Object.keys(content)) {
            clipboardItemData[type] = this.getBlob(content, type);
        }
        return [new ClipboardItem(clipboardItemData)];
    }
    getBlob(clipboardContent, type) {
        const content = clipboardContent[type];
        if (content instanceof Blob || content instanceof File) {
            return content;
        }
        return new Blob([clipboardContent[type] || ""], {
            type,
        });
    }
}

// -----------------------------------------------------------------------------
// SpreadSheet
// -----------------------------------------------------------------------------
const CARET_DOWN_SVG = /*xml*/ `
<svg xmlns='http://www.w3.org/2000/svg' width='7' height='4' viewBox='0 0 7 4'>
  <polygon fill='%23374151' points='3.5 4 7 0 0 0'/>
</svg>
`;
css /* scss */ `
  .o-spreadsheet {
    position: relative;
    display: grid;
    font-size: 14px;

    .text-muted {
      color: ${TEXT_BODY_MUTED} !important;
    }
    .o-disabled {
      opacity: 0.4;
      cursor: default;
      pointer-events: none;
    }

    &,
    *,
    *:before,
    *:after {
      /* rtl not supported ATM */
      direction: ltr;
    }
    .o-separator {
      border-bottom: ${MENU_SEPARATOR_BORDER_WIDTH}px solid ${SEPARATOR_COLOR};
      margin-top: ${MENU_SEPARATOR_PADDING}px;
      margin-bottom: ${MENU_SEPARATOR_PADDING}px;
    }
    .o-hoverable-button {
      border-radius: 2px;
      cursor: pointer;
      .o-icon {
        color: ${TEXT_BODY};
      }
      &:not(.o-disabled):not(.active):hover {
        background-color: ${BUTTON_HOVER_BG};
        color: ${BUTTON_HOVER_TEXT_COLOR};
        .o-icon {
          color: ${BUTTON_HOVER_TEXT_COLOR};
        }
      }
      &.active {
        background-color: ${BUTTON_ACTIVE_BG};
        color: ${BUTTON_ACTIVE_TEXT_COLOR};
        .o-icon {
          color: ${BUTTON_ACTIVE_TEXT_COLOR};
        }
      }
    }

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

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

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

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

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

    .o-input {
      min-width: 0px;
      padding: 1px 0;
      width: 100%;
      outline: none;
      border-color: ${GRAY_300};
      color: ${GRAY_900};

      &::placeholder {
        opacity: 0.5;
      }
      &:focus {
        border-color: ${ACTION_COLOR};
      }
    }

    select.o-input {
      cursor: pointer;
      border-width: 0 0 1px 0;
      padding: 1px 6px 1px 0px;

      appearance: none;
      -webkit-appearance: none;
      -moz-appearance: none;
      background: transparent url("data:image/svg+xml,${encodeURIComponent(CARET_DOWN_SVG)}")
        no-repeat right center;
      text-overflow: ellipsis;

      &:disabled {
        color: ${DISABLED_TEXT_COLOR};
        opacity: 0.4;
        cursor: default;
      }
    }

    .o-input[type="text"] {
      border-width: 0 0 1px 0;
    }

    .o-input[type="number"],
    .o-number-input {
      border-width: 0 0 1px 0;
      /* Remove number input arrows */
      appearance: textfield;
      &::-webkit-outer-spin-button,
      &::-webkit-inner-spin-button {
        -webkit-appearance: none;
        margin: 0;
      }
    }
  }

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

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

    > canvas {
      box-sizing: content-box;
      border-bottom: 1px solid #e2e3e3;
      border-right: 1px solid #e2e3e3;
    }

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

  .o-button {
    border: 1px solid;
    border-radius: 4px;
    font-weight: 500;
    font-size: 14px;
    min-height: 32px;
    line-height: 16px;
    flex-grow: 1;
    background-color: ${BUTTON_BG};
    border: 1px solid ${GRAY_200};
    color: ${TEXT_BODY};

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

    &.primary {
      background-color: ${PRIMARY_BUTTON_BG};
      border-color: ${PRIMARY_BUTTON_BG};
      color: #fff;
      &:hover:enabled {
        color: #fff;
        background-color: ${PRIMARY_BUTTON_HOVER_BG};
      }
      &:active:enabled {
        background-color: ${PRIMARY_BUTTON_ACTIVE_BG};
        color: ${PRIMARY_BUTTON_BG};
      }
      &.o-disabled,
      &:disabled {
        opacity: 0.5;
      }
    }

    &:hover:enabled {
      color: ${BUTTON_HOVER_TEXT_COLOR};
      background-color: ${BUTTON_HOVER_BG};
    }
    &:active:enabled {
      color: ${BUTTON_ACTIVE_TEXT_COLOR};
      background-color: ${BUTTON_ACTIVE_BG};
    }

    &.o-disabled,
    &:disabled {
      opacity: 0.8;
    }

    &.o-button-danger:hover {
      color: #ffffff;
      background: ${ALERT_DANGER_BORDER};
    }
  }

  .o-button-link {
    cursor: pointer;
    text-decoration: none;
    color: ${ACTION_COLOR};
    font-weight: 500;
    &:hover,
    &:active {
      color: ${ACTION_COLOR_HOVER};
    }
  }

  .o-button-icon {
    cursor: pointer;
    color: ${TEXT_BODY_MUTED};
    font-weight: 500;
    &:hover,
    &:active {
      color: ${TEXT_BODY};
    }
  }

  .o-spreadsheet-bottombar-wrapper {
    z-index: ${ComponentsImportance.ScrollBar + 1};
  }
`;
class Spreadsheet extends Component {
    static template = "o-spreadsheet-Spreadsheet";
    static props = {
        model: Object,
        notifyUser: { type: Function, optional: true },
        raiseError: { type: Function, optional: true },
        askConfirmation: { type: Function, optional: true },
    };
    static components = {
        TopBar,
        Grid,
        BottomBar,
        SmallBottomBar,
        SidePanels,
        SpreadsheetDashboard,
        HeaderGroupContainer,
        FullScreenFigure,
    };
    sidePanel;
    spreadsheetRef = useRef("spreadsheet");
    spreadsheetRect = useSpreadsheetRect();
    _focusGrid;
    isViewportTooSmall = false;
    notificationStore;
    composerFocusStore;
    get model() {
        return this.props.model;
    }
    getStyle() {
        const properties = {};
        if (this.env.isDashboard()) {
            properties["grid-template-rows"] = `auto`;
        }
        else {
            properties["grid-template-rows"] = `min-content auto min-content`;
        }
        const columnWidth = this.sidePanel.mainPanel
            ? `${this.sidePanel.totalPanelSize || DEFAULT_SIDE_PANEL_SIZE}px`
            : "auto";
        properties["grid-template-columns"] = `auto ${columnWidth}`;
        return cssPropertiesToCss(properties);
    }
    setup() {
        if (!("isSmall" in this.env)) {
            const screenSize = useScreenWidth();
            useSubEnv({
                get isSmall() {
                    return screenSize.isSmall;
                },
            });
        }
        const stores = useStoreProvider();
        stores.inject(ModelStore, this.model);
        const env = this.env;
        stores.get(ScreenWidthStore).setSmallThreshhold(() => {
            return env.isSmall;
        });
        this.notificationStore = useStore(NotificationStore);
        this.composerFocusStore = useStore(ComposerFocusStore);
        this.sidePanel = useStore(SidePanelStore);
        const fileStore = this.model.config.external.fileStore;
        useSubEnv({
            model: this.model,
            imageProvider: fileStore ? new ImageProvider(fileStore) : undefined,
            loadCurrencies: this.model.config.external.loadCurrencies,
            loadLocales: this.model.config.external.loadLocales,
            isDashboard: () => this.model.getters.isDashboard(),
            openSidePanel: this.sidePanel.open.bind(this.sidePanel),
            replaceSidePanel: this.sidePanel.replace.bind(this.sidePanel),
            toggleSidePanel: this.sidePanel.toggle.bind(this.sidePanel),
            clipboard: this.env.clipboard || instantiateClipboard(),
            startCellEdition: (content) => this.composerFocusStore.focusActiveComposer({ content }),
            notifyUser: (notification) => this.notificationStore.notifyUser(notification),
            askConfirmation: (text, confirm, cancel) => this.notificationStore.askConfirmation(text, confirm, cancel),
            raiseError: (text, cb) => this.notificationStore.raiseError(text, cb),
            isMobile: isMobileOS,
        });
        this.notificationStore.updateNotificationCallbacks({ ...this.props });
        useEffect(() => {
            /**
             * Only refocus the grid if the active element is not a child of the spreadsheet
             * (i.e. activeElement is outside of the spreadsheetRef component)
             * and spreadsheet is a child of that element. Anything else means that the focus
             * is on an element that needs to keep it.
             */
            if (!this.spreadsheetRef.el.contains(document.activeElement) &&
                document.activeElement?.contains(this.spreadsheetRef.el)) {
                this.focusGrid();
            }
        });
        useExternalListener(window, "resize", () => this.render(true));
        // 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
        useExternalListener(document.body, "wheel", () => { });
        this.bindModelEvents();
        onWillUpdateProps((nextProps) => {
            if (nextProps.model !== this.props.model) {
                throw new Error("Changing the props model is not supported at the moment.");
            }
            if (nextProps.notifyUser !== this.props.notifyUser ||
                nextProps.askConfirmation !== this.props.askConfirmation ||
                nextProps.raiseError !== this.props.raiseError) {
                this.notificationStore.updateNotificationCallbacks({ ...nextProps });
            }
        });
        const render = batched(this.render.bind(this, true));
        onMounted(() => {
            this.checkViewportSize();
            stores.on("store-updated", this, render);
            resizeObserver.observe(this.spreadsheetRef.el);
        });
        onWillUnmount(() => {
            this.unbindModelEvents();
            stores.off("store-updated", this);
            resizeObserver.disconnect();
            unregisterChartJsExtensions();
        });
        onPatched(() => {
            this.checkViewportSize();
        });
        const resizeObserver = new ResizeObserver(() => {
            this.sidePanel.changeSpreadsheetWidth(this.spreadsheetRect.width);
        });
    }
    bindModelEvents() {
        this.model.on("update", this, () => this.render(true));
        this.model.on("notify-ui", this, (notification) => this.notificationStore.notifyUser(notification));
        this.model.on("raise-error-ui", this, ({ text }) => this.notificationStore.raiseError(text));
    }
    unbindModelEvents() {
        this.model.off("update", this);
        this.model.off("notify-ui", this);
        this.model.off("raise-error-ui", this);
    }
    checkViewportSize() {
        const { xRatio, yRatio } = this.env.model.getters.getFrozenSheetViewRatio(this.env.model.getters.getActiveSheetId());
        if (!isFinite(xRatio) || !isFinite(yRatio)) {
            // before mounting, the ratios can be NaN or Infinity if the viewport size is 0
            return;
        }
        if (yRatio > MAXIMAL_FREEZABLE_RATIO || xRatio > MAXIMAL_FREEZABLE_RATIO) {
            if (this.isViewportTooSmall) {
                return;
            }
            this.notificationStore.notifyUser({
                text: _t("The current window is too small to display this sheet properly. Consider resizing your browser window or adjusting frozen rows and columns."),
                type: "warning",
                sticky: false,
            });
            this.isViewportTooSmall = true;
        }
        else {
            this.isViewportTooSmall = false;
        }
    }
    focusGrid() {
        if (!this._focusGrid) {
            return;
        }
        this._focusGrid();
    }
    get gridHeight() {
        return this.env.model.getters.getSheetViewDimension().height;
    }
    get gridContainerStyle() {
        const gridColSize = GROUP_LAYER_WIDTH * this.rowLayers.length;
        const gridRowSize = GROUP_LAYER_WIDTH * this.colLayers.length;
        return cssPropertiesToCss({
            "grid-template-columns": `${gridColSize ? gridColSize + 2 : 0}px auto`, // +2: margins
            "grid-template-rows": `${gridRowSize ? gridRowSize + 2 : 0}px auto`,
        });
    }
    get rowLayers() {
        const sheetId = this.env.model.getters.getActiveSheetId();
        return this.env.model.getters.getVisibleGroupLayers(sheetId, "ROW");
    }
    get colLayers() {
        const sheetId = this.env.model.getters.getActiveSheetId();
        return this.env.model.getters.getVisibleGroupLayers(sheetId, "COL");
    }
    getGridSize() {
        const el = this.spreadsheetRef.el;
        if (!el) {
            return { width: 0, height: 0 };
        }
        const getHeight = (selector) => el.querySelector(selector)?.getBoundingClientRect().height || 0;
        const getWidth = (selector) => el.querySelector(selector)?.getBoundingClientRect().width || 0;
        const rect = el.getBoundingClientRect();
        const topBarHeight = getHeight(".o-spreadsheet-topbar-wrapper");
        const bottomBarHeight = getHeight(".o-spreadsheet-bottombar-wrapper");
        const colGroupHeight = getHeight(".o-column-groups");
        const gridWidth = getWidth(".o-grid");
        const width = Math.max(gridWidth - SCROLLBAR_WIDTH, 0);
        const height = Math.max(rect.height - topBarHeight - bottomBarHeight - colGroupHeight - SCROLLBAR_WIDTH, 0);
        return { width, height };
    }
}

class ReadonlyTransportFilter {
    transportService;
    constructor(transportService) {
        this.transportService = transportService;
    }
    async sendMessage(message) {
        if (message.type === "CLIENT_JOINED" ||
            message.type === "CLIENT_LEFT" ||
            message.type === "CLIENT_MOVED") {
            await this.transportService.sendMessage(message);
        }
        // ignore all other messages
    }
    onNewMessage(id, callback) {
        this.transportService.onNewMessage(id, callback);
    }
    leave(id) {
        this.transportService.leave(id);
    }
}

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

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

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

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

/**
 * The tree is a data structure used to maintain the different branches of the
 * SelectiveHistory.
 *
 * Branches can be "stacked" on each other and an execution path can be derived
 * from any stack of branches. The rules to derive this path is explained below.
 *
 * An operation can be cancelled/undone by inserting a new branch below
 * this operation.
 * e.g
 *    Given the branch A    B   C
 *    To undo B, a new branching branch is inserted at operation B.
 *    ```txt
 *    A   B   C   D
 *        >   C'  D'
 *    ```
 *    A new execution path can now be derived. At each operation:
 *    - if there is a lower branch, don't execute it and go to the operation below
 *    - if not, execute it and go to the operation on the right.
 *    The execution path is   A   C'    D'
 *    Operation C and D have been adapted (transformed) in the lower branch
 *    since operation B is not executed in this branch.
 *
 */
class Tree {
    buildTransformation;
    branches;
    branchingOperationIds = new Map();
    constructor(buildTransformation, initialBranch) {
        this.buildTransformation = buildTransformation;
        this.branches = [initialBranch];
    }
    /**
     * Return the last branch of the entire stack of branches.
     */
    getLastBranch() {
        return this.branches[this.branches.length - 1];
    }
    /**
     * Return the sequence of operations from this branch
     * until the very last branch.
     */
    execution(branch) {
        return new OperationSequence(linkNext(this._execution(branch), this._execution(branch)));
    }
    /**
     * Return the sequence of operations from this branch
     * to the very first branch.
     */
    revertedExecution(branch) {
        return new OperationSequence(linkNext(this._revertedExecution(branch), this._revertedExecution(branch)));
    }
    /**
     * Append an operation to the end of the tree.
     * Also insert the (transformed) operation in all previous branches.
     *
     * Adding operation `D` to the last branch
     * ```txt
     *  A1   B1   C1
     *  >    B2   C2
     * ```
     * will give
     * ```txt
     *  A1   B1   C1   D'   with D' = D transformed with A1
     *  >    B2   C2   D
     * ```
     */
    insertOperationLast(branch, operation) {
        const insertAfter = branch.getLastOperationId() || this.previousBranch(branch)?.getLastOperationId();
        branch.append(operation);
        if (insertAfter) {
            this.insertPrevious(branch, operation, insertAfter);
        }
    }
    /**
     * Insert a new operation after an other operation.
     * The operation will be inserted in this branch, in next branches (transformed)
     * and in previous branches (also transformed).
     *
     * Given
     * ```txt
     *  1: A1   B1   C1
     *  2: >    B2   C2
     *  3:      >    C3
     * ```
     * Inserting D to branch 2 gives
     * ```txt
     *  1: A1   B1   C1   D1          D1 = D transformed with A1
     *  2: >    B2   C2   D     with  D  = D
     *  3:      >    C3   D2          D2 = D transformed without B2 (B2⁻¹)
     * ```
     */
    insertOperationAfter(branch, operation, predecessorOpId) {
        branch.insert(operation, predecessorOpId);
        this.updateNextWith(branch, operation, predecessorOpId);
        this.insertPrevious(branch, operation, predecessorOpId);
    }
    /**
     * Create a new branching branch at the given operation.
     * This cancels the operation from the execution path.
     */
    undo(branch, operation) {
        const transformation = this.buildTransformation.without(operation.data);
        const branchingId = this.branchingOperationIds.get(branch);
        this.branchingOperationIds.set(branch, operation.id);
        const nextBranch = branch.fork(operation.id);
        if (branchingId) {
            this.branchingOperationIds.set(nextBranch, branchingId);
        }
        this.insertBranchAfter(branch, nextBranch);
        this.transform(nextBranch, transformation);
    }
    /**
     * Remove the branch just after this one. This un-cancels (redo) the branching
     * operation. Lower branches will be transformed accordingly.
     *
     * Given
     * ```txt
     *  1: A1   B1   C1
     *  2: >    B2   C2
     *  3:      >    C3
     * ```
     * removing the next branch of 1 gives
     *
     * ```txt
     *  1: A1   B1   C1
     *  2:      >    C3'   with  C3' = C1 transformed without B1 (B1⁻¹)
     * ```
     */
    redo(branch) {
        const removedBranch = this.nextBranch(branch);
        if (!removedBranch)
            return;
        const nextBranch = this.nextBranch(removedBranch);
        this.removeBranchFromTree(removedBranch);
        const undoBranchingId = this.branchingOperationIds.get(removedBranch);
        if (undoBranchingId) {
            this.branchingOperationIds.set(branch, undoBranchingId);
        }
        else {
            this.branchingOperationIds.delete(branch);
        }
        if (nextBranch) {
            this.rebaseUp(nextBranch);
        }
    }
    /**
     * Drop the operation and all following operations in every
     * 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 {
    HEAD_BRANCH;
    HEAD_OPERATION;
    tree;
    applyOperation;
    revertOperation;
    buildEmpty;
    buildTransformation;
    /**
     * The selective history is a data structure used to register changes/updates of a state.
     * Each change/update is called an "operation".
     * The data structure allows to easily cancel (and redo) any operation individually.
     * An operation can be represented by any data structure. It can be a "command", a "diff", etc.
     * However it must have the following properties:
     * - it can be applied to modify the state
     * - it can be reverted on the state such that it was never executed.
     * - it can be transformed given other operation (Operational Transformation)
     *
     * Since this data structure doesn't know anything about the state nor the structure of
     * operations, the actual work must be performed by external functions given as parameters.
     * @param initialOperationId
     * @param applyOperation a function which can apply an operation to the state
     * @param revertOperation  a function which can revert an operation from the state
     * @param buildEmpty  a function returning an "empty" operation.
     *                    i.e an operation that leaves the state unmodified once applied or reverted
     *                    (used for internal implementation)
     * @param buildTransformation Factory used to build transformations
     */
    constructor(args) {
        this.applyOperation = args.applyOperation;
        this.revertOperation = args.revertOperation;
        this.buildEmpty = args.buildEmpty;
        this.buildTransformation = args.buildTransformation;
        this.HEAD_BRANCH = new Branch(this.buildTransformation);
        this.tree = new Tree(this.buildTransformation, this.HEAD_BRANCH);
        const initialOperationId = args.initialOperationId;
        const initial = new Operation(initialOperationId, this.buildEmpty(initialOperationId));
        this.tree.insertOperationLast(this.HEAD_BRANCH, initial);
        this.HEAD_OPERATION = initial;
    }
    /**
     * Return the operation identified by its id.
     */
    get(operationId) {
        return this.tree.findOperation(this.HEAD_BRANCH, operationId).operation.data;
    }
    /**
     * Append a new operation as the last one
     */
    append(operationId, data) {
        const operation = new Operation(operationId, data);
        const branch = this.tree.getLastBranch();
        this.tree.insertOperationLast(branch, operation);
        this.HEAD_BRANCH = branch;
        this.HEAD_OPERATION = operation;
    }
    /**
     * Insert a new operation after a specific operation (may not be the last operation).
     * Following operations will be transformed according
     * to the new operation.
     */
    insert(operationId, data, insertAfter) {
        const operation = new Operation(operationId, data);
        this.revertTo(insertAfter);
        this.tree.insertOperationAfter(this.HEAD_BRANCH, operation, insertAfter);
        this.fastForward();
    }
    /**
     * @param operationId operation to undo
     * @param undoId the id of the "undo operation"
     * @param insertAfter the id of the operation after which to insert the undo
     */
    undo(operationId, undoId, insertAfter) {
        const { branch, operation } = this.tree.findOperation(this.HEAD_BRANCH, operationId);
        this.revertBefore(operationId);
        this.tree.undo(branch, operation);
        this.fastForward();
        this.insert(undoId, this.buildEmpty(undoId), insertAfter);
    }
    /**
     * @param operationId operation to redo
     * @param redoId the if of the "redo operation"
     * @param insertAfter the id of the operation after which to insert the redo
     */
    redo(operationId, redoId, insertAfter) {
        const { branch } = this.tree.findOperation(this.HEAD_BRANCH, operationId);
        this.revertBefore(operationId);
        this.tree.redo(branch);
        this.fastForward();
        this.insert(redoId, this.buildEmpty(redoId), insertAfter);
    }
    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(args) {
    return new SelectiveHistory({
        initialOperationId: args.initialRevisionId,
        applyOperation: (revision) => {
            const commands = revision.commands.slice();
            const { changes } = args.recordChanges(() => {
                for (const command of commands) {
                    args.dispatch(command);
                }
            });
            revision.setChanges(changes);
        },
        revertOperation: (revision) => revertChanges([revision]),
        buildEmpty: (id) => new Revision(id, "empty", []),
        buildTransformation: {
            with: (revision) => (toTransform) => {
                return new Revision(toTransform.id, toTransform.clientId, transformAll(toTransform.commands, revision.commands), toTransform.rootCommand, undefined, toTransform.timestamp);
            },
            without: (revision) => (toTransform) => {
                return new Revision(toTransform.id, toTransform.clientId, transformAll(toTransform.commands, revision.commands.map(inverseCommand).flat()), toTransform.rootCommand, undefined, toTransform.timestamp);
            },
        },
    });
}
/**
 * Revert changes from the given revisions
 */
function revertChanges(revisions) {
    for (const revision of revisions.slice().reverse()) {
        for (let i = revision.changes.length - 1; i >= 0; i--) {
            const change = revision.changes[i];
            applyChange(change);
        }
    }
}
/**
 * Apply the changes of the given HistoryChange to the state
 */
function applyChange(change) {
    const target = change.target;
    const key = change.key;
    const before = change.before;
    if (before === undefined) {
        delete target[key];
    }
    else {
        target[key] = before;
    }
}

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

/**
 * Processes all selection updates (usually from user inputs) and emits an event
 * with the new selected anchor
 */
class SelectionStreamProcessorImpl {
    getters;
    stream;
    /**
     * "Active" anchor used as a reference to compute new anchors
     * An new initial value is given each time the stream is
     * captured. The value is updated with each new anchor.
     */
    anchor;
    defaultAnchor;
    constructor(getters) {
        this.getters = getters;
        this.stream = new EventStream();
        this.anchor = { cell: { col: 0, row: 0 }, zone: positionToZone({ col: 0, row: 0 }) };
        this.defaultAnchor = this.anchor;
    }
    capture(owner, anchor, callbacks) {
        this.stream.capture(owner, callbacks);
        this.anchor = anchor;
    }
    /**
     * Register as default subscriber and capture the event stream.
     */
    registerAsDefault(owner, anchor, callbacks) {
        this.checkAnchorZoneOrThrow(anchor);
        this.stream.registerAsDefault(owner, callbacks);
        this.defaultAnchor = anchor;
        this.capture(owner, anchor, callbacks);
    }
    resetDefaultAnchor(owner, anchor) {
        this.checkAnchorZoneOrThrow(anchor);
        if (this.stream.isListening(owner)) {
            this.anchor = anchor;
        }
        this.defaultAnchor = anchor;
    }
    resetAnchor(owner, anchor) {
        this.checkAnchorZoneOrThrow(anchor);
        if (this.stream.isListening(owner)) {
            this.anchor = anchor;
        }
    }
    observe(owner, callbacks) {
        this.stream.observe(owner, callbacks);
    }
    release(owner) {
        if (this.stream.isListening(owner)) {
            this.stream.release(owner);
            this.anchor = this.defaultAnchor;
        }
    }
    getBackToDefault() {
        this.stream.getBackToDefault();
    }
    modifyAnchor(anchor, mode, options) {
        const sheetId = this.getters.getActiveSheetId();
        anchor = {
            ...anchor,
            zone: this.getters.expandZone(sheetId, anchor.zone),
        };
        return this.processEvent({
            options,
            anchor,
            mode,
        });
    }
    /**
     * Select a new anchor
     */
    selectZone(anchor, options = { scrollIntoView: true }) {
        return this.modifyAnchor(anchor, "overrideSelection", options);
    }
    /**
     * Select a single cell as the new anchor.
     */
    selectCell(col, row) {
        const zone = positionToZone({ col, row });
        return this.selectZone({ zone, cell: { col, row } }, { scrollIntoView: true });
    }
    /**
     * Set the selection to one of the cells adjacent to the current anchor cell.
     */
    moveAnchorCell(direction, step = 1) {
        if (step !== "end" && step <= 0) {
            return new DispatchResult("InvalidSelectionStep" /* CommandResult.InvalidSelectionStep */);
        }
        const { col, row } = this.getNextAvailablePosition(direction, step);
        return this.selectCell(col, row);
    }
    /**
     * Update the current anchor such that it includes the given
     * cell position.
     */
    setAnchorCorner(col, row) {
        const sheetId = this.getters.getActiveSheetId();
        const { col: anchorCol, row: anchorRow } = this.anchor.cell;
        const zone = {
            left: Math.min(anchorCol, col),
            top: Math.min(anchorRow, row),
            right: Math.max(anchorCol, col),
            bottom: Math.max(anchorRow, row),
        };
        const expandedZone = this.getters.expandZone(sheetId, zone);
        const anchor = { zone: expandedZone, cell: { col: anchorCol, row: anchorRow } };
        return this.processEvent({
            mode: "updateAnchor",
            anchor: anchor,
            options: { scrollIntoView: false },
        });
    }
    /**
     * Add a new cell to the current selection
     */
    addCellToSelection(col, row) {
        const sheetId = this.getters.getActiveSheetId();
        ({ col, row } = this.getters.getMainCellPosition({ sheetId, col, row }));
        const zone = this.getters.expandZone(sheetId, positionToZone({ col, row }));
        return this.processEvent({
            options: { scrollIntoView: true },
            anchor: { zone, cell: { col, row } },
            mode: "newAnchor",
        });
    }
    /**
     * Increase or decrease the size of the current anchor zone.
     * The anchor cell remains where it is. It's the opposite side
     * of the anchor zone which moves.
     */
    resizeAnchorZone(direction, step = 1) {
        if (step !== "end" && step <= 0) {
            return new DispatchResult("InvalidSelectionStep" /* CommandResult.InvalidSelectionStep */);
        }
        const sheetId = this.getters.getActiveSheetId();
        const anchor = this.anchor;
        const { col: anchorCol, row: anchorRow } = anchor.cell;
        const { left, right, top, bottom } = anchor.zone;
        const starting = this.getStartingPosition(direction);
        const [deltaCol, deltaRow] = this.deltaToTarget(starting, direction, step);
        if (deltaCol === 0 && deltaRow === 0) {
            return DispatchResult.Success;
        }
        let result = anchor.zone;
        const expand = (z) => {
            z = reorderZone(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 ? reorderZone(result) : result;
            if (result && !isEqual(result, anchor.zone)) {
                return this.processEvent({
                    options: { scrollIntoView: true },
                    mode: "updateAnchor",
                    anchor: { zone: result, cell: { col: anchorCol, row: anchorRow } },
                });
            }
        }
        const currentZone = {
            top: anchorRow,
            bottom: anchorRow,
            left: anchorCol,
            right: anchorCol,
        };
        const zoneWithDelta = reorderZone({
            top: this.getNextAvailableRow(deltaRow, refCol, top),
            left: this.getNextAvailableCol(deltaCol, left, refRow),
            bottom: this.getNextAvailableRow(deltaRow, refCol, bottom),
            right: this.getNextAvailableCol(deltaCol, right, refRow),
        });
        result = expand(union(currentZone, zoneWithDelta));
        const newAnchor = { zone: result, cell: { col: anchorCol, row: anchorRow } };
        return this.processEvent({
            anchor: newAnchor,
            mode: "updateAnchor",
            options: { scrollIntoView: true },
        });
    }
    selectColumn(index, mode) {
        const sheetId = this.getters.getActiveSheetId();
        const bottom = this.getters.getNumberRows(sheetId) - 1;
        let zone = { left: index, right: index, top: 0, bottom };
        const top = this.getters.findFirstVisibleColRowIndex(sheetId, "ROW");
        let col, row;
        switch (mode) {
            case "overrideSelection":
            case "newAnchor":
                col = index;
                row = top;
                break;
            case "updateAnchor":
                ({ col, row } = this.anchor.cell);
                zone = union(zone, { left: col, right: col, top, bottom });
                break;
        }
        return this.processEvent({
            options: {
                scrollIntoView: false,
                unbounded: true,
            },
            anchor: { zone, cell: { col, row } },
            mode,
        });
    }
    selectRow(index, mode) {
        const sheetId = this.getters.getActiveSheetId();
        const right = this.getters.getNumberCols(sheetId) - 1;
        let zone = { top: index, bottom: index, left: 0, right };
        const left = this.getters.findFirstVisibleColRowIndex(sheetId, "COL");
        let col, row;
        switch (mode) {
            case "overrideSelection":
            case "newAnchor":
                col = left;
                row = index;
                break;
            case "updateAnchor":
                ({ col, row } = this.anchor.cell);
                zone = union(zone, { left, right, top: row, bottom: row });
                break;
        }
        return this.processEvent({
            options: {
                scrollIntoView: false,
                unbounded: true,
            },
            anchor: { zone, cell: { col, row } },
            mode,
        });
    }
    /**
     * Loop the current selection while keeping the same anchor. The selection will loop through:
     *  1) the smallest zone that contain the anchor and that have only empty cells bordering it
     *  2) the whole sheet
     *  3) the anchor cell
     */
    loopSelection() {
        const sheetId = this.getters.getActiveSheetId();
        const anchor = this.anchor;
        // The whole sheet is selected, select the anchor cell
        if (isEqual(this.anchor.zone, this.getters.getSheetZone(sheetId))) {
            return this.modifyAnchor({ ...anchor, zone: positionToZone(anchor.cell) }, "updateAnchor", {
                scrollIntoView: false,
            });
        }
        const tableZone = this.getters.getContiguousZone(sheetId, anchor.zone);
        return !deepEquals(tableZone, anchor.zone)
            ? this.modifyAnchor({ ...anchor, zone: tableZone }, "updateAnchor", {
                scrollIntoView: false,
            })
            : this.selectAll();
    }
    /**
     * Select a "table" around the current selection.
     * We define a table by the smallest zone that contain the anchor and that have only empty
     * cells bordering it
     */
    selectTableAroundSelection() {
        const sheetId = this.getters.getActiveSheetId();
        const tableZone = this.getters.getContiguousZone(sheetId, this.anchor.zone);
        return this.modifyAnchor({ ...this.anchor, zone: tableZone }, "updateAnchor", {
            scrollIntoView: false,
        });
    }
    /**
     * Select the entire sheet
     */
    selectAll() {
        const sheetId = this.getters.getActiveSheetId();
        const bottom = this.getters.getNumberRows(sheetId) - 1;
        const right = this.getters.getNumberCols(sheetId) - 1;
        const zone = { left: 0, top: 0, bottom, right };
        return this.processEvent({
            mode: "overrideSelection",
            anchor: { zone, cell: this.anchor.cell },
            options: {
                scrollIntoView: false,
            },
        });
    }
    isListening(owner) {
        return this.stream.isListening(owner);
    }
    /**
     * Process a new anchor selection event. If the new anchor is inside
     * the sheet boundaries, the event is pushed to the event stream to
     * be processed.
     */
    processEvent(newAnchorEvent) {
        const event = { ...newAnchorEvent, previousAnchor: deepCopy(this.anchor) };
        const commandResult = this.checkEventAnchorZone(event);
        if (commandResult !== "Success" /* CommandResult.Success */) {
            return new DispatchResult(commandResult);
        }
        this.anchor = event.anchor;
        this.stream.send(event);
        return DispatchResult.Success;
    }
    checkEventAnchorZone(event) {
        return this.checkAnchorZone(event.anchor);
    }
    checkAnchorZone(anchor) {
        const { cell, zone } = anchor;
        if (!isInside(cell.col, cell.row, zone)) {
            return "InvalidAnchorZone" /* CommandResult.InvalidAnchorZone */;
        }
        const { left, right, top, bottom } = zone;
        const sheetId = this.getters.getActiveSheetId();
        const refCol = this.getters.findVisibleHeader(sheetId, "COL", left, right);
        const refRow = this.getters.findVisibleHeader(sheetId, "ROW", top, bottom);
        if (refRow === undefined || refCol === undefined) {
            return "SelectionOutOfBound" /* CommandResult.SelectionOutOfBound */;
        }
        return "Success" /* CommandResult.Success */;
    }
    checkAnchorZoneOrThrow(anchor) {
        const result = this.checkAnchorZone(anchor);
        if (result === "InvalidAnchorZone" /* CommandResult.InvalidAnchorZone */) {
            throw new Error("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 sheetId = this.getters.getActiveSheetId();
        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);
        const mode = !this.isCellSkippableInCluster({ ...currentPosition, sheetId }) &&
            !this.isCellSkippableInCluster({ ...nextCellPosition, sheetId })
            ? "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.isCellSkippableInCluster({ ...nextCellPosition, sheetId });
            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;
    }
    /** 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 };
        startingPosition[dimOfInterest] =
            dimension === "cols"
                ? this.getNextAvailableCol(direction, startingPosition.col, startingPosition.row)
                : this.getNextAvailableRow(direction, startingPosition.col, startingPosition.row);
        return { col: startingPosition.col, row: startingPosition.row };
    }
    getPosition() {
        return { ...this.anchor.cell };
    }
    isCellSkippableInCluster(position) {
        const mainPosition = this.getters.getMainCellPosition(position);
        const cell = this.getters.getEvaluatedCell(mainPosition);
        return (cell.type === CellValueType.empty || (cell.type === CellValueType.text && cell.value === ""));
    }
}

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

class StateObserver {
    changes;
    commands = [];
    /**
     * Record the changes which could happen in the given callback, save them in a
     * new revision with the given id and userId.
     */
    recordChanges(callback) {
        this.changes = [];
        this.commands = [];
        callback();
        return { changes: this.changes, commands: this.commands };
    }
    addCommand(command) {
        this.commands.push(command);
    }
    addChange(...args) {
        const val = args.pop();
        const root = args[0];
        let value = root;
        const key = args.at(-1);
        const pathLength = args.length - 2;
        for (let pathIndex = 1; pathIndex <= pathLength; pathIndex++) {
            const p = args[pathIndex];
            if (value[p] === undefined) {
                const nextPath = args[pathIndex + 1];
                value[p] = createEmptyStructure(nextPath);
            }
            value = value[p];
        }
        if (value[key] === val) {
            return;
        }
        this.changes?.push({
            key,
            target: value,
            before: value[key],
        });
        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 secondaryCatAxId = 17781238;
const valAxId = 88853993;
const secondaryValAxId = 88853994;
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?.text) {
        const titleColor = toXlsxHexColor(chartMutedFontColor(chart.data.backgroundColor));
        const fontSize = chart.data.title.fontSize ?? CHART_TITLE_FONT_SIZE;
        title = escapeXml /*xml*/ `
      <c:title>
        ${insertText(chart.data.title.text, titleColor, fontSize, chart.data.title)}
        <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 "combo":
            plot = addComboChart(chart.data);
            break;
        case "pyramid":
            plot = addPyramidChart(chart.data);
            break;
        case "line":
            plot = addLineChart(chart.data);
            break;
        case "scatter":
            plot = addScatterChart(chart.data);
            break;
        case "pie":
            plot = addDoughnutChart(chart.data, chartSheetIndex, data);
            break;
        case "radar":
            plot = addRadarChart(chart.data);
    }
    let position = "none";
    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>
        ${position !== "none" ? 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 = CHART_TITLE_FONT_SIZE, style = {}) {
    return escapeXml /*xml*/ `
    <c:tx>
      <c:rich>
        <a:bodyPr />
        <a:lstStyle />
        <a:p>
          <a:pPr lvl="0">
            <a:defRPr b="${style?.bold ? 1 : 0}" i="${style?.italic ? 1 : 0}">
              ${solidFill(fontColor)}
              <a:latin typeface="+mn-lt"/>
            </a:defRPr>
          </a:pPr>
          <a:r> <!-- Runs -->
            <a:rPr b="${style?.bold ? 1 : 0}" i="${style?.italic ? 1 : 0}" 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 extractTrendline(trend, dataSetColor) {
    if (!trend)
        return escapeXml /*xml*/ ``;
    const { type, order, window } = trend;
    const trendLineNodes = [];
    switch (type) {
        case "poly":
            if (order && order > 1) {
                trendLineNodes.push(escapeXml /*xml*/ `<c:trendlineType val="poly" />`);
                trendLineNodes.push(escapeXml /*xml*/ `<c:order val="${order}" />`);
            }
            else {
                trendLineNodes.push(escapeXml /*xml*/ `<c:trendlineType val="linear" />`);
            }
            break;
        case "movingAvg":
            trendLineNodes.push(escapeXml /*xml*/ `<c:trendlineType val="movingAvg" />`);
            if (window) {
                trendLineNodes.push(escapeXml /*xml*/ `<c:period val="${window}" />`);
            }
            break;
        default:
            trendLineNodes.push(escapeXml /*xml*/ `<c:trendlineType val="${type}" />`);
            break;
    }
    return escapeXml /*xml*/ `
    <c:trendline>
      ${extractTrendlineCommonAttributes(trend, dataSetColor)}
      ${joinXmlNodes(trendLineNodes)}
    </c:trendline>
  `;
}
function extractTrendlineCommonAttributes(trend, dataSetColor) {
    if (!trend) {
        return escapeXml /*xml*/ ``;
    }
    const attrs = [
        ["val", trend.color ? toXlsxHexColor(trend.color).slice(-6) : getTrendlineColor(dataSetColor)],
    ];
    return escapeXml /*xml*/ `
    <c:spPr>
      <a:ln w="19050" cap="rnd">
          <a:solidFill>
              <a:srgbClr ${formatAttributes(attrs)}/>
          </a:solidFill>
          <a:prstDash val="sysDot" />
      </a:ln>
      <a:effectLst />
    </c:spPr>
    <c:dispRSqr val="0" />
    <c:dispEq val="0" />
  `;
}
function getTrendlineColor(dataSetColor) {
    return toXlsxHexColor(lightenColor(dataSetColor, 0.5));
}
function extractDataSetLabel(label) {
    if (!label) {
        return escapeXml /*xml*/ ``;
    }
    if ("text" in label && label.text) {
        return escapeXml /*xml*/ `
      <c:tx><c:v>${label.text}</c:v></c:tx>
    `;
    }
    if ("reference" in label && label.reference) {
        return escapeXml /*xml*/ `
      <c:tx>
      ${stringRef(label.reference)}
      </c:tx>
    `;
    }
    return escapeXml /*xml*/ ``;
}
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 chartDirection = chart.horizontal ? "bar" : "col";
    const dataSetsColors = chart.dataSets.map((ds) => ds.backgroundColor ?? "");
    const colors = new ColorGenerator(chart.dataSets.length, dataSetsColors);
    const leftDataSetsNodes = [];
    const rightDataSetsNodes = [];
    for (const [dsIndex, dataset] of Object.entries(chart.dataSets)) {
        const color = toXlsxHexColor(colors.next());
        const dataShapeProperty = shapeProperty({
            backgroundColor: color,
            line: { color },
        });
        const dataSetNode = escapeXml /*xml*/ `
      <c:ser>
        <c:idx val="${dsIndex}"/>
        <c:order val="${dsIndex}"/>
        ${extractTrendline(dataset.trend, color)}
        ${extractDataSetLabel(dataset.label)}
        ${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>
    `;
        if (dataset.rightYAxis) {
            rightDataSetsNodes.push(dataSetNode);
        }
        else {
            leftDataSetsNodes.push(dataSetNode);
        }
    }
    const grouping = chart.stacked ? "stacked" : "clustered";
    const overlap = chart.stacked ? 100 : -20;
    return escapeXml /*xml*/ `
  ${leftDataSetsNodes.length
        ? escapeXml /*xml*/ `
        <c:barChart>
          <c:barDir val="${chartDirection}"/>
          <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(leftDataSetsNodes)}
          <c:axId val="${catAxId}" />
          <c:axId val="${valAxId}" />
        </c:barChart>
        ${chartDirection === "col"
            ? addAx("b", "c:catAx", catAxId, valAxId, chart.axesDesign?.x?.title, chart.fontColor)
            : addAx("b", "c:catAx", catAxId, valAxId, chart.axesDesign?.y?.title, chart.fontColor, undefined, "maxMin")}
        ${chartDirection === "col"
            ? addAx("l", "c:valAx", valAxId, catAxId, chart.axesDesign?.y?.title, chart.fontColor)
            : addAx("l", "c:valAx", valAxId, catAxId, chart.axesDesign?.x?.title, chart.fontColor, undefined, undefined, "max")}
      `
        : ""}
  ${rightDataSetsNodes.length
        ? 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(rightDataSetsNodes)}
          <c:axId val="${catAxId + 1}" />
          <c:axId val="${valAxId + 1}" />
        </c:barChart>
        ${addAx("b", "c:catAx", catAxId + 1, valAxId + 1, chart.axesDesign?.x?.title, chart.fontColor, leftDataSetsNodes.length ? 1 : 0)}
        ${addAx("r", "c:valAx", valAxId + 1, catAxId + 1, chart.axesDesign?.y1?.title, chart.fontColor)}
      `
        : ""}`;
}
function addComboChart(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 dataSets = chart.dataSets;
    const dataSetsColors = dataSets.map((ds) => ds.backgroundColor ?? "");
    const colors = new ColorGenerator(dataSets.length, dataSetsColors);
    let dataSet = dataSets[0];
    const firstColor = toXlsxHexColor(colors.next());
    const useRightAxisForBarSerie = dataSet.rightYAxis ?? false;
    const barDataSetNode = escapeXml /*xml*/ `
    <c:ser>
      <c:idx val="0"/>
      <c:order val="0"/>
      ${extractTrendline(dataSet.trend, firstColor)}
      ${extractDataSetLabel(dataSet.label)}
      ${shapeProperty({
        backgroundColor: firstColor,
        line: { color: firstColor },
    })}
      ${chart.labelRange ? escapeXml /*xml*/ `<c:cat>${stringRef(chart.labelRange)}</c:cat>` : ""}
      <!-- x-coordinate values -->
      <c:val>
        ${numberRef(dataSet.range)}
      </c:val>
    </c:ser>
  `;
    const leftDataSetsNodes = [];
    const rightDataSetsNodes = [];
    for (let dsIndex = 1; dsIndex < dataSets.length; dsIndex++) {
        dataSet = dataSets[dsIndex];
        const color = toXlsxHexColor(colors.next());
        const dataShapeProperty = shapeProperty({
            backgroundColor: color,
            line: { color },
        });
        const dataSetNode = 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"/>
          ${dataShapeProperty}
        </c:marker>
        ${extractTrendline(dataSet.trend, color)}
        ${extractDataSetLabel(dataSet.label)}
        ${dataShapeProperty}
        ${chart.labelRange ? escapeXml `<c:cat>${stringRef(chart.labelRange)}</c:cat>` : ""}
        <!-- x-coordinate values -->
        <c:val>
          ${numberRef(dataSet.range)}
        </c:val>
      </c:ser>
    `;
        if (dataSet.rightYAxis) {
            rightDataSetsNodes.push(dataSetNode);
        }
        else {
            leftDataSetsNodes.push(dataSetNode);
        }
    }
    const overlap = chart.stacked ? 100 : -20;
    return escapeXml /*xml*/ `
    <c:barChart>
      <c:barDir val="col"/>
      <c:grouping val="clustered"/>
      <c:overlap val="${overlap}"/>
      <c:gapWidth val="70"/>
      <!-- each data marker in the series does not have a different color -->
      <c:varyColors val="0"/>
      ${barDataSetNode}
      <c:axId val="${useRightAxisForBarSerie ? secondaryCatAxId : catAxId}" />
      <c:axId val="${useRightAxisForBarSerie ? secondaryValAxId : valAxId}" />
    </c:barChart>
    ${leftDataSetsNodes.length
        ? escapeXml /*xml*/ `
        <c:lineChart>
          <c:grouping val="standard"/>
          <!-- each data marker in the series does not have a different color -->
          <c:varyColors val="0"/>
          ${joinXmlNodes(leftDataSetsNodes)}
          <c:axId val="${catAxId}" />
          <c:axId val="${valAxId}" />
        </c:lineChart>
      `
        : ""}
    ${rightDataSetsNodes.length
        ? escapeXml /*xml*/ `
        <c:lineChart>
          <c:grouping val="standard"/>
          <!-- each data marker in the series does not have a different color -->
          <c:varyColors val="0"/>
          ${joinXmlNodes(rightDataSetsNodes)}
          <c:axId val="${secondaryCatAxId}" />
          <c:axId val="${secondaryValAxId}" />
        </c:lineChart>
      `
        : ""}
    ${!useRightAxisForBarSerie || leftDataSetsNodes.length
        ? escapeXml /*xml*/ `
        ${addAx("b", "c:catAx", catAxId, valAxId, chart.axesDesign?.x?.title, chart.fontColor, 0)}
        ${addAx("l", "c:valAx", valAxId, catAxId, chart.axesDesign?.y?.title, chart.fontColor)}
      `
        : ""}
    ${useRightAxisForBarSerie || rightDataSetsNodes.length
        ? escapeXml /*xml*/ `
        ${addAx("b", "c:catAx", secondaryCatAxId, secondaryValAxId, chart.axesDesign?.x?.title, chart.fontColor, leftDataSetsNodes.length || !useRightAxisForBarSerie ? 1 : 0)}
        ${addAx("r", "c:valAx", secondaryValAxId, secondaryCatAxId, chart.axesDesign?.y1?.title, chart.fontColor)}
      `
        : ""}
  `;
}
function addPyramidChart(chart) {
    const dataSets = chart.dataSets;
    const dataSetsColors = dataSets.map((ds) => ds.backgroundColor ?? "");
    const colors = new ColorGenerator(dataSets.length, dataSetsColors);
    const leftDataSet = dataSets[0];
    const rightDataSet = dataSets[1];
    const firstColor = toXlsxHexColor(colors.next());
    const secondColor = toXlsxHexColor(colors.next());
    const { maxValue, majorUnit } = getPyramidChartHorizontalAxisConfig(chart.maxValue);
    const labelRangeEl = chart.labelRange
        ? escapeXml `<c:cat>${stringRef(chart.labelRange)}</c:cat>`
        : "";
    const leftBarDataSetNode = escapeXml /*xml*/ `
  <c:ser>
    <c:idx val="0"/>
    <c:order val="0"/>
    <c:invertIfNegative val="0" />
    ${extractDataSetLabel(leftDataSet.label)}
    ${shapeProperty({
        backgroundColor: firstColor,
        line: { color: firstColor },
    })}
    ${labelRangeEl}
    <!-- x-coordinate values -->
    <c:val>
      ${numberRef(leftDataSet.range)}
    </c:val>
  </c:ser>
`;
    const rightBarDataSetNode = escapeXml /*xml*/ `
  <c:ser>
    <c:idx val="1"/>
    <c:order val="1"/>
    <c:invertIfNegative val="0" />
    ${extractDataSetLabel(rightDataSet.label)}
    ${shapeProperty({
        backgroundColor: secondColor,
        line: { color: secondColor },
    })}
    ${chart.labelRange ? escapeXml /*xml*/ `<c:cat>${stringRef(chart.labelRange)}</c:cat>` : ""}
    <!-- x-coordinate values -->
    <c:val>
      ${numberRef(rightDataSet.range)}
    </c:val>
  </c:ser>
`;
    return escapeXml /*xml*/ `
    <c:barChart>
      <c:barDir val="bar"/>
      <c:grouping val="clustered"/>
      <c:varyColors val="0" />
      ${leftBarDataSetNode}
      <c:gapWidth val="50" />
      <c:axId val="${catAxId}" />
      <c:axId val="${valAxId}" />
    </c:barChart>
    <c:barChart>
      <c:barDir val="bar"/>
      <c:grouping val="clustered"/>
      <c:varyColors val="0" />
      ${rightBarDataSetNode}
      <c:gapWidth val="50" />
      <c:axId val="${secondaryCatAxId}" />
      <c:axId val="${secondaryValAxId}" />
    </c:barChart>
    ${addAx("r", "c:catAx", catAxId, valAxId, chart.axesDesign?.y?.title, chart.fontColor, 0, "maxMin", "autoZero", "high")}
    ${addAx("b", "c:valAx", valAxId, catAxId, chart.axesDesign?.x?.title, chart.fontColor, 0, "maxMin", "max", "nextTo", maxValue, majorUnit, "#0;#0")}
    ${addAx("t", "c:valAx", secondaryValAxId, secondaryCatAxId, undefined, chart.fontColor, 1)}
    ${addAx("l", "c:catAx", secondaryCatAxId, secondaryValAxId, undefined, chart.fontColor, 1, "maxMin")}
  `;
}
function getPyramidChartHorizontalAxisConfig(maxValue) {
    const adjustMaxToDivisibleBy = (value, divisor) => {
        let adjusted = Math.ceil(value);
        while (adjusted % divisor !== 0) {
            adjusted++;
        }
        return adjusted;
    };
    const tickCount = 4;
    const interval = tickCount - 1;
    const adjustedMax = adjustMaxToDivisibleBy(maxValue, interval);
    const majorUnit = adjustedMax / interval;
    return {
        maxValue: adjustedMax,
        majorUnit,
    };
}
function addLineChart(chart) {
    const dataSetsColors = chart.dataSets.map((ds) => ds.backgroundColor ?? "");
    const colors = new ColorGenerator(chart.dataSets.length, dataSetsColors);
    const leftDataSetsNodes = [];
    const rightDataSetsNodes = [];
    for (const [dsIndex, dataset] of Object.entries(chart.dataSets)) {
        const color = toXlsxHexColor(colors.next());
        const dataShapeProperty = shapeProperty({
            line: {
                width: 2.5,
                style: "solid",
                color,
            },
        });
        const dataSetNode = 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"/>
          ${shapeProperty({ backgroundColor: color, line: { color } })}
        </c:marker>
        ${extractTrendline(dataset.trend, color)}
        ${extractDataSetLabel(dataset.label)}
        ${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>
    `;
        if (dataset.rightYAxis) {
            rightDataSetsNodes.push(dataSetNode);
        }
        else {
            leftDataSetsNodes.push(dataSetNode);
        }
    }
    const grouping = chart.stacked ? "stacked" : "standard";
    return escapeXml /*xml*/ `
    ${leftDataSetsNodes.length
        ? 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(leftDataSetsNodes)}
          <c:axId val="${catAxId}" />
          <c:axId val="${valAxId}" />
        </c:lineChart>
        ${addAx("b", "c:catAx", catAxId, valAxId, chart.axesDesign?.x?.title, chart.fontColor)}
        ${addAx("l", "c:valAx", valAxId, catAxId, chart.axesDesign?.y?.title, chart.fontColor)}
      `
        : ""}
    ${rightDataSetsNodes.length
        ? 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(rightDataSetsNodes)}
          <c:axId val="${catAxId + 1}" />
          <c:axId val="${valAxId + 1}" />
        </c:lineChart>
        ${addAx("b", "c:catAx", catAxId + 1, valAxId + 1, chart.axesDesign?.x?.title, chart.fontColor, leftDataSetsNodes.length ? 1 : 0)}
        ${addAx("r", "c:valAx", valAxId + 1, catAxId + 1, chart.axesDesign?.y1?.title, chart.fontColor)}
      `
        : ""}
  `;
}
function addScatterChart(chart) {
    const dataSetsColors = chart.dataSets.map((ds) => ds.backgroundColor ?? "");
    const colors = new ColorGenerator(chart.dataSets.length, dataSetsColors);
    const leftDataSetsNodes = [];
    const rightDataSetsNodes = [];
    for (const [dsIndex, dataset] of Object.entries(chart.dataSets)) {
        const color = toXlsxHexColor(colors.next());
        const dataSetNode = escapeXml /*xml*/ `
      <c:ser>
        <c:idx val="${dsIndex}"/>
        <c:order val="${dsIndex}"/>
        <c:smooth val="0"/>
        <c:spPr>
          <a:ln w="19050" cap="rnd">
            <a:noFill/>
            <a:round/>
          </a:ln>
          <a:effectLst/>
        </c:spPr>
        <c:marker>
          <c:symbol val="circle" />
          <c:size val="5"/>
          ${shapeProperty({ backgroundColor: color, line: { color } })}
        </c:marker>
        ${extractTrendline(dataset.trend, color)}
        ${extractDataSetLabel(dataset.label)}
        ${chart.labelRange
            ? escapeXml /*xml*/ `<c:xVal> <!-- x-coordinate values -->
              ${numberRef(chart.labelRange)}
            </c:xVal>`
            : ""}
        <c:yVal> <!-- y-coordinate values -->
          ${numberRef(dataset.range)}
        </c:yVal>
      </c:ser>
    `;
        if (dataset.rightYAxis) {
            rightDataSetsNodes.push(dataSetNode);
        }
        else {
            leftDataSetsNodes.push(dataSetNode);
        }
    }
    return escapeXml /*xml*/ `
  ${leftDataSetsNodes.length
        ? escapeXml /*xml*/ `
      <c:scatterChart>
        <!-- each data marker in the series does not have a different color -->
        <c:varyColors val="0"/>
        <c:scatterStyle val="lineMarker"/>
        ${joinXmlNodes(leftDataSetsNodes)}
        <c:axId val="${catAxId}" />
        <c:axId val="${valAxId}" />
      </c:scatterChart>
      ${addAx("b", "c:valAx", catAxId, valAxId, chart.axesDesign?.x?.title, chart.fontColor)}
      ${addAx("l", "c:valAx", valAxId, catAxId, chart.axesDesign?.y?.title, chart.fontColor)}
    `
        : ""}
  ${rightDataSetsNodes.length
        ? escapeXml /*xml*/ `
      <c:scatterChart>
        <!-- each data marker in the series does not have a different color -->
        <c:varyColors val="0"/>
        <c:scatterStyle val="lineMarker"/>
        ${joinXmlNodes(rightDataSetsNodes)}
        <c:axId val="${catAxId + 1}" />
        <c:axId val="${valAxId + 1}" />
      </c:scatterChart>
      ${addAx("b", "c:valAx", catAxId + 1, valAxId + 1, chart.axesDesign?.x?.title, chart.fontColor, leftDataSetsNodes.length ? 1 : 0)}
      ${addAx("r", "c:valAx", valAxId + 1, catAxId + 1, chart.axesDesign?.y1?.title, chart.fontColor)}
    `
        : ""}`;
}
function addRadarChart(chart) {
    const dataSetsColors = chart.dataSets.map((ds) => ds.backgroundColor ?? "");
    const colors = new ColorGenerator(chart.dataSets.length, dataSetsColors);
    const dataSetsNodes = [];
    for (const [dsIndex, dataset] of Object.entries(chart.dataSets)) {
        const color = toXlsxHexColor(colors.next());
        const dataShapeProperty = shapeProperty({
            line: {
                width: 2.5,
                style: "solid",
                color,
            },
        });
        const dataSetNode = 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"/>
          ${shapeProperty({ backgroundColor: color, line: { color } })}
        </c:marker>
        ${extractDataSetLabel(dataset.label)}
        ${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>
    `;
        dataSetsNodes.push(dataSetNode);
    }
    return escapeXml /*xml*/ `
    ${escapeXml /*xml*/ `
        <c:radarChart>
        <c:radarStyle val="marker"/>
          <c:varyColors val="0"/>
          ${joinXmlNodes(dataSetsNodes)}
          <c:axId val="${catAxId}" />
          <c:axId val="${valAxId}" />
        </c:radarChart>
        ${addAx("b", "c:catAx", catAxId, valAxId, chart.axesDesign?.x?.title, chart.fontColor)}
        ${addAx("l", "c:valAx", valAxId, catAxId, chart.axesDesign?.y?.title, chart.fontColor)}
      `}
  `;
}
function addDoughnutChart(chart, chartSheetIndex, data) {
    const maxLength = largeMax(chart.dataSets.map((ds) => getRangeSize(ds.range, chartSheetIndex, data)));
    const colors = new ColorGenerator(maxLength);
    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}"/>
        ${extractDataSetLabel(dataset.label)}
        ${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="${chart.pieHolePercentage ?? (chart.isDoughnut ? DEFAULT_DOUGHNUT_CHART_HOLE_SIZE : 0)}" />
      ${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, title, defaultFontColor, deleteAxis = 0, orientation = "minMax", crossPosition, tickLabelPosition = "nextTo", maxValue, majorUnit, format = "General") {
    // 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.
    const color = title?.color ? toXlsxHexColor(title.color) : defaultFontColor;
    const fontSize = title?.fontSize ?? CHART_AXIS_TITLE_FONT_SIZE;
    const crossBetweenEl = axisName === "c:valAx" ? escapeXml `<c:crossBetween val="between" />` : "";
    const maxValueEl = maxValue ? escapeXml `<c:max val="${maxValue}" />` : "";
    const minValueEl = maxValue ? escapeXml `<c:min val="${-maxValue}" />` : "";
    const majorUnitEl = majorUnit ? escapeXml `<c:majorUnit val="${majorUnit}" />` : "";
    return escapeXml /*xml*/ `
    <${axisName}>
      <c:axId val="${axId}"/>
      <c:crossAx val="${crossAxId}"/> <!-- reference to the other axe of the chart -->
      <c:crosses val="${crossPosition || (position === "b" || position === "l" ? "min" : "max")}"/>
      <c:auto val="1"/>
      ${crossBetweenEl}
      <c:delete val="${deleteAxis}"/> <!-- by default, axis are not displayed -->
      <c:scaling>
        <c:orientation  val="${orientation}" />
        ${maxValueEl}
        ${minValueEl}
      </c:scaling>
      ${majorUnitEl}
      <c:axPos val="${position}" />
      <c:tickLblPos val="${tickLabelPosition}" />
      ${insertMajorGridLines()}
      <c:majorTickMark val="out" />
      <c:minorTickMark val="none" />
      <c:numFmt formatCode="${format}" sourceLinked="${format === "General" ? "1" : "0"}" />
      <c:title>
        ${insertText(title?.text ?? "", color, fontSize, title)}
      </c:title>
      ${insertTextProperties(10, defaultFontColor)}
    </${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(formula, value, formulaSpillRange) {
    if (!formula) {
        return { attrs: [], node: escapeXml `` };
    }
    const type = getCellType(value);
    if (type === undefined) {
        return { attrs: [], node: escapeXml `` };
    }
    const attrs = [
        ["cm", "1"],
        ["t", type],
    ];
    const XlsxFormula = adaptFormulaToExcel(formula);
    const exportedValue = adaptFormulaValueToExcel(value);
    // We treat all formulas as array formulas (a simple formula
    // is an array formula that spills on only one cell) to avoid
    // trying to detect spilling sub-formulas which is not a trivial task.
    const node = escapeXml /*xml*/ `<f t="array" ref="${formulaSpillRange}">${XlsxFormula}</f><v>${exportedValue}</v>`;
    return { attrs, node };
}
function addContent(content, sharedStrings, forceString = false) {
    let value = content;
    const attrs = [];
    const clearValue = value.trim().toUpperCase();
    if (!forceString && ["TRUE", "FALSE"].includes(clearValue)) {
        value = clearValue === "TRUE" ? "1" : "0";
        attrs.push(["t", "b"]);
    }
    else if (forceString || !isNumber(value, DEFAULT_LOCALE)) {
        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,
                    tokenEndIndex: 0,
                    tokenStartIndex: 0,
                });
            }
        }
    }
    return { ...ast, args };
}
/**
 * Prepend function names that are not compatible with Old Excel versions
 */
function prependNonRetrocompatibleFunction(ast) {
    const formulaName = ast.value.toUpperCase();
    return {
        ...ast,
        value: NON_RETROCOMPATIBLE_FUNCTIONS.includes(formulaName)
            ? `_xlfn.${formulaName}`
            : formulaName,
    };
}
/**
 * Convert strings that correspond to a date to the format YYYY-DD-MM
 */
function convertDateFormat(ast) {
    const value = ast.value.replace(new RegExp('"', "g"), "");
    const internalDate = parseDateTime(value, DEFAULT_LOCALE);
    if (internalDate) {
        const format = [];
        if (mdyDateRegexp.test(value) || ymdDateRegexp.test(value)) {
            format.push("yyyy-mm-dd");
        }
        if (timeRegexp.test(value)) {
            format.push("hh:mm:ss");
        }
        return {
            ...ast,
            value: formatValue(internalDate.value, { format: format.join(" "), locale: DEFAULT_LOCALE }),
        };
    }
    else {
        return { ...ast, value: ast.value.replace(/\\"/g, `""`) };
    }
}

function addConditionalFormatting(dxfs, conditionalFormats) {
    // Conditional Formats
    const cfNodes = [];
    for (const cf of conditionalFormats) {
        // Special case for each type of rule: might be better to extract that logic in dedicated functions
        switch (cf.rule.type) {
            case "CellIsRule":
                cfNodes.push(addCellIsRule(cf, cf.rule, dxfs));
                break;
            case "ColorScaleRule":
                cfNodes.push(addColorScaleRule(cf, cf.rule));
                break;
            case "IconSetRule":
                cfNodes.push(addIconSetRule(cf, cf.rule));
                break;
            case "DataBarRule":
                cfNodes.push(addDataBarRule(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));
    if (operator.length) {
        ruleAttributes.push(["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 "notContainsText":
            return [`ISERROR(SEARCH("${values[0]}",${firstCell}))`];
        case "beginsWithText":
            return [`LEFT(${firstCell},LEN("${values[0]}"))="${values[0]}"`];
        case "endsWithText":
            return [`RIGHT(${firstCell},LEN("${values[0]}"))="${values[0]}"`];
        case "isEmpty":
            return [`LEN(TRIM(${firstCell}))=0`];
        case "isNotEmpty":
            return [`LEN(TRIM(${firstCell}))>0`];
        case "isEqual":
        case "isNotEqual":
        case "isGreaterThan":
        case "isGreaterOrEqualTo":
        case "isLessThan":
        case "isLessOrEqualTo":
            return [values[0]];
        case "customFormula":
            return values[0].startsWith("=") ? [values[0].slice(1)] : [values[0]];
        case "isBetween":
        case "isNotBetween":
            return [values[0], values[1]];
    }
}
function cellRuleTypeAttributes(rule) {
    const operator = convertOperator(rule.operator);
    switch (rule.operator) {
        case "containsText":
        case "notContainsText":
        case "beginsWithText":
        case "endsWithText":
            return [
                ["type", operator],
                ["text", rule.values[0]],
            ];
        case "isEmpty":
        case "isNotEmpty":
            return [["type", operator]];
        case "isEqual":
        case "isNotEqual":
        case "isGreaterThan":
        case "isGreaterOrEqualTo":
        case "isLessThan":
        case "isLessOrEqualTo":
        case "isBetween":
        case "isNotBetween":
            return [["type", "cellIs"]];
        case "customFormula":
            return [["type", "expression"]];
    }
}
function addDataBarRule(cf, rule) {
    const ruleAttributes = commonCfAttributes(cf);
    ruleAttributes.push(["type", "dataBar"]);
    // TODO ATM we do not support min and max values, so to have the same result
    // in Excel, we export with min=0 and max=100
    return escapeXml /*xml*/ `
    <conditionalFormatting sqref="${cf.ranges.join(" ")}">
      <cfRule ${formatAttributes(ruleAttributes)}>
        <dataBar>
          <cfvo type="min" val="0"/>
          <cfvo type="max" val="100"/>
          <color rgb="${toXlsxHexColor(colorNumberToHex(rule.color))}"/>
        </dataBar>
      </cfRule>
    </conditionalFormatting>
  `;
}
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 (const 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(colorNumberToHex(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 (const 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 addDataValidationRules(dataValidationRules) {
    const dvRulesCount = dataValidationRules.length;
    if (dvRulesCount === 0) {
        return [];
    }
    const dvNodes = [new XMLString(`<dataValidations count="${dvRulesCount}">`)];
    for (const dvRule of dataValidationRules) {
        switch (dvRule.criterion.type) {
            case "dateIs":
            case "dateIsBefore":
            case "dateIsOnOrBefore":
            case "dateIsAfter":
            case "dateIsOnOrAfter":
            case "dateIsBetween":
            case "dateIsNotBetween":
                dvNodes.push(addDateRule(dvRule));
                break;
            case "isEqual":
            case "isNotEqual":
            case "isGreaterThan":
            case "isGreaterOrEqualTo":
            case "isLessThan":
            case "isLessOrEqualTo":
            case "isBetween":
            case "isNotBetween":
                dvNodes.push(addDecimalRule(dvRule));
                break;
            case "isValueInRange":
            case "isValueInList":
                dvNodes.push(addListRule(dvRule));
                break;
            case "customFormula":
                dvNodes.push(addCustomFormulaRule(dvRule));
                break;
            default:
                console.warn(`Data validation ${dvRule.criterion.type} is not supported in xlsx.`);
                break;
        }
    }
    dvNodes.push(new XMLString("</dataValidations>"));
    return dvNodes;
}
function addDateRule(dvRule) {
    const rule = dvRule.criterion;
    const formula1 = adaptFormulaToExcel(rule.values[0]);
    const formula2 = rule.values[1] ? adaptFormulaToExcel(rule.values[1]) : undefined;
    const operator = convertDateCriterionTypeToExcelOperator(dvRule.criterion.type);
    const attributes = commonDataValidationAttributes(dvRule);
    attributes.push(["type", "date"], ["operator", operator]);
    if (formula2) {
        return escapeXml /*xml*/ `
      <dataValidation ${formatAttributes(attributes)}>
        <formula1>${toNumber(formula1, DEFAULT_LOCALE)}</formula1>
        <formula2>${toNumber(formula2, DEFAULT_LOCALE)}</formula2>
      </dataValidation>
    `;
    }
    return escapeXml /*xml*/ `
    <dataValidation ${formatAttributes(attributes)}>
      <formula1>${toNumber(formula1, DEFAULT_LOCALE)}</formula1>
    </dataValidation>
  `;
}
function addDecimalRule(dvRule) {
    const rule = dvRule.criterion;
    const formula1 = adaptFormulaToExcel(rule.values[0]);
    const formula2 = rule.values[1] ? adaptFormulaToExcel(rule.values[1]) : undefined;
    const operator = convertDecimalCriterionTypeToExcelOperator(dvRule.criterion.type);
    const attributes = commonDataValidationAttributes(dvRule);
    attributes.push(["type", "decimal"], ["operator", operator]);
    if (formula2) {
        return escapeXml /*xml*/ `
      <dataValidation ${formatAttributes(attributes)}>
        <formula1>${formula1}</formula1>
        <formula2>${formula2}</formula2>
      </dataValidation>
    `;
    }
    return escapeXml /*xml*/ `
    <dataValidation ${formatAttributes(attributes)}>
      <formula1>${formula1}</formula1>
    </dataValidation>
  `;
}
function addListRule(dvRule) {
    const rule = dvRule.criterion;
    const formula1 = dvRule.criterion.type === "isValueInRange"
        ? adaptFormulaToExcel(rule.values[0])
        : `"${rule.values.join(",")}"`;
    const attributes = commonDataValidationAttributes(dvRule);
    attributes.push(["type", "list"]);
    return escapeXml /*xml*/ `
    <dataValidation ${formatAttributes(attributes)}>
      <formula1>${formula1}</formula1>
    </dataValidation>
  `;
}
function addCustomFormulaRule(dvRule) {
    const rule = dvRule.criterion;
    const formula1 = adaptFormulaToExcel(rule.values[0]);
    const attributes = commonDataValidationAttributes(dvRule);
    attributes.push(["type", "custom"]);
    return escapeXml /*xml*/ `
    <dataValidation ${formatAttributes(attributes)}>
      <formula1>${formula1}</formula1>
    </dataValidation>
  `;
}
function commonDataValidationAttributes(dvRule) {
    return [
        ["allowBlank", "1"],
        ["showInputMessage", "1"],
        ["showErrorMessage", "1"],
        ["errorStyle", !dvRule.isBlocking ? "warning" : ""],
        ["sqref", dvRule.ranges.join(" ")],
    ];
}

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

function addNumberFormats(numFmts) {
    const numFmtNodes = [];
    for (const [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 (const 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 (const border of Object.values(borders)) {
        borderNodes.push(escapeXml /*xml*/ `
      <border>
        <left ${formatBorderAttribute(border["left"])}>
          ${addBorderColor(border["left"])}
        </left>
        <right ${formatBorderAttribute(border["right"])}>
          ${addBorderColor(border["right"])}
        </right>
        <top ${formatBorderAttribute(border["top"])}>
          ${addBorderColor(border["top"])}
        </top>
        <bottom ${formatBorderAttribute(border["bottom"])}>
          ${addBorderColor(border["bottom"])}
        </bottom>
        <diagonal ${formatBorderAttribute(border["diagonal"])}>
          ${addBorderColor(border["diagonal"])}
        </diagonal>
      </border>
    `);
    }
    return escapeXml /*xml*/ `
    <borders count="${borders.length}">
      ${joinXmlNodes(borderNodes)}
    </borders>
  `;
}
function formatBorderAttribute(description) {
    if (!description) {
        return escapeXml ``;
    }
    return formatAttributes([["style", description.style]]);
}
function addBorderColor(description) {
    if (!description) {
        return escapeXml ``;
    }
    return escapeXml /*xml*/ `
    <color ${formatAttributes([["rgb", toXlsxHexColor(description.color.rgb)]])}/>
  `;
}
function addStyles(styles) {
    const styleNodes = [];
    for (const style of styles) {
        const attributes = [
            ["numFmtId", style.numFmtId],
            ["fillId", style.fillId],
            ["fontId", style.fontId],
            ["borderId", style.borderId],
        ];
        // Note: the apply${substyleName} does not seem to be required
        const alignAttrs = [];
        if (style.alignment && style.alignment.vertical) {
            alignAttrs.push(["vertical", style.alignment.vertical]);
        }
        if (style.alignment && style.alignment.horizontal) {
            alignAttrs.push(["horizontal", style.alignment.horizontal]);
        }
        if (style.alignment && style.alignment.wrapText) {
            alignAttrs.push(["wrapText", "1"]);
        }
        if (style.alignment && style.alignment.shrinkToFit) {
            alignAttrs.push(["shrinkToFit", "1"]);
        }
        if (alignAttrs.length > 0) {
            attributes.push(["applyAlignment", "1"]); // for Libre Office
            styleNodes.push(escapeXml /*xml*/ `<xf ${formatAttributes(attributes)}><alignment ${formatAttributes(alignAttrs)} /></xf> `);
        }
        else {
            styleNodes.push(escapeXml /*xml*/ `<xf ${formatAttributes(attributes)} />`);
        }
    }
    return escapeXml /*xml*/ `
    <cellXfs count="${styles.length}">
      ${joinXmlNodes(styleNodes)}
    </cellXfs>
  `;
}
/**
 * DXFS : Differential Formatting Records - Conditional formats
 */
function addCellWiseConditionalFormatting(dxfs // cell-wise CF
) {
    const dxfNodes = [];
    for (const dxf of dxfs) {
        let fontNode = escapeXml ``;
        if (dxf.font) {
            fontNode = addFont(dxf.font);
        }
        let fillNode = escapeXml ``;
        if (dxf.fill) {
            fillNode = escapeXml /*xml*/ `
        <fill>
          <patternFill>
            <bgColor rgb="${toXlsxHexColor(dxf.fill.fgColor.rgb)}" />
          </patternFill>
        </fill>
      `;
        }
        dxfNodes.push(escapeXml /*xml*/ `
      <dxf>
        ${fontNode}
        ${fillNode}
      </dxf>
    `);
    }
    return escapeXml /*xml*/ `
    <dxfs count="${dxfs.length}">
      ${joinXmlNodes(dxfNodes)}
    </dxfs>
  `;
}

function createTable(table, tableId, sheetData) {
    const tableAttributes = [
        ["id", tableId],
        ["name", `Table${tableId}`],
        ["displayName", `Table${tableId}`],
        ["ref", table.range],
        ["headerRowCount", table.config.numberOfHeaders],
        ["totalsRowCount", table.config.totalRow ? 1 : 0],
        ["xmlns", NAMESPACE.table],
        ["xmlns:xr", NAMESPACE.revision],
        ["xmlns:xr3", NAMESPACE.revision3],
        ["xmlns:mc", NAMESPACE.markupCompatibility],
    ];
    const xml = escapeXml /*xml*/ `
    <table ${formatAttributes(tableAttributes)}>
      ${table.config.hasFilters ? addAutoFilter(table) : ""}
      ${addTableColumns(table, sheetData)}
      ${addTableStyle(table)}
    </table>
    `;
    return parseXML(xml);
}
function addAutoFilter(table) {
    const autoFilterAttributes = [["ref", table.range]];
    return escapeXml /*xml*/ `
  <autoFilter ${formatAttributes(autoFilterAttributes)}>
    ${joinXmlNodes(addFilterColumns(table))}
  </autoFilter>
  `;
}
function addFilterColumns(table) {
    const columns = [];
    for (const filter of table.filters) {
        const colXml = escapeXml /*xml*/ `
      <filterColumn ${formatAttributes([["colId", filter.colId]])}>
        ${addFilter(filter)}
      </filterColumn>
      `;
        columns.push(colXml);
    }
    return columns;
}
function addFilter(filter) {
    const filterValues = filter.displayedValues.map((val) => escapeXml /*xml*/ `<filter ${formatAttributes([["val", val]])}/>`);
    const filterAttributes = filter.displayBlanks ? [["blank", 1]] : [];
    return escapeXml /*xml*/ `
  <filters ${formatAttributes(filterAttributes)}>
      ${joinXmlNodes(filterValues)}
  </filters>
`;
}
function addTableColumns(table, sheetData) {
    const tableZone = toZone(table.range);
    const columns = [];
    for (const i of range(0, zoneToDimension(tableZone).numberOfCols)) {
        const colHeaderXc = toXC(tableZone.left + i, tableZone.top);
        const colName = sheetData.cells[colHeaderXc] || `col${i}`;
        const colAttributes = [
            ["id", i + 1], // id cannot be 0
            ["name", colName],
        ];
        if (table.config.totalRow) {
            // Note: To be 100% complete, we could also add a `totalsRowLabel` attribute for total strings, and a tag
            // `<totalsRowFormula>` for the formula of the total. But those doesn't seem to be mandatory for Excel.
            const colTotalXc = toXC(tableZone.left + i, tableZone.bottom);
            const colTotalContent = sheetData.cells[colTotalXc];
            if (colTotalContent?.startsWith("=")) {
                colAttributes.push(["totalsRowFunction", "custom"]);
            }
        }
        columns.push(escapeXml /*xml*/ `<tableColumn ${formatAttributes(colAttributes)}/>`);
    }
    return escapeXml /*xml*/ `
        <tableColumns ${formatAttributes([["count", columns.length]])}>
            ${joinXmlNodes(columns)}
        </tableColumns>
    `;
}
function addTableStyle(table) {
    const tableStyleAttrs = [
        ["name", table.config.styleId],
        ["showFirstColumn", table.config.firstColumn ? 1 : 0],
        ["showLastColumn", table.config.lastColumn ? 1 : 0],
        ["showRowStripes", table.config.bandedRows ? 1 : 0],
        ["showColumnStripes", table.config.bandedColumns ? 1 : 0],
    ];
    return escapeXml /*xml*/ `<tableStyleInfo ${formatAttributes(tableStyleAttrs)}/>`;
}

function addColumns(cols) {
    if (!Object.values(cols).length) {
        return escapeXml ``;
    }
    const colNodes = [];
    for (const [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],
        ];
        if (col.outlineLevel) {
            attributes.push(["outlineLevel", col.outlineLevel]);
        }
        if (col.collapsed) {
            attributes.push(["collapsed", 1]);
        }
        colNodes.push(escapeXml /*xml*/ `
      <col ${formatAttributes(attributes)}/>
    `);
    }
    return escapeXml /*xml*/ `
    <cols>
      ${joinXmlNodes(colNodes)}
    </cols>
  `;
}
function addRows(construct, data, sheet) {
    const rowNodes = [];
    const styles = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.styles));
    const borders = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.borders));
    const formats = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.formats));
    for (let r = 0; r < sheet.rowNumber; r++) {
        const rowAttrs = [["r", r + 1]];
        const row = sheet.rows[r] || {};
        if (row.size && row.size !== DEFAULT_CELL_HEIGHT) {
            rowAttrs.push(["ht", convertHeightToExcel(row.size)], ["customHeight", 1]);
        }
        if (row.isHidden) {
            rowAttrs.push(["hidden", 1]);
        }
        if (row.outlineLevel) {
            rowAttrs.push(["outlineLevel", row.outlineLevel]);
        }
        if (row.collapsed) {
            rowAttrs.push(["collapsed", 1]);
        }
        const cellNodes = [];
        for (let c = 0; c < sheet.colNumber; c++) {
            const xc = toXC(c, r);
            const content = sheet.cells[xc];
            const value = sheet.cellValues[xc];
            const position = { sheetId: sheet.id, col: c, row: r };
            const styleId = styles.get(position);
            const formatId = formats.get(position);
            const borderId = borders.get(position);
            if (content || styleId || formatId || borderId || value !== undefined) {
                const attributes = [["r", xc]];
                // style
                const id = normalizeStyle(construct, extractStyle(data, content, styleId, formatId, borderId));
                // don't add style if default
                if (id) {
                    attributes.push(["s", id]);
                }
                let additionalAttrs = [];
                let cellNode = escapeXml ``;
                // Either formula or static value inside the cell
                if (content?.startsWith("=") && value !== undefined) {
                    const res = addFormula(content, value, sheet.formulaSpillRanges[xc] ?? xc);
                    if (!res) {
                        continue;
                    }
                    ({ attrs: additionalAttrs, node: cellNode } = res);
                }
                else if (content && isMarkdownLink(content)) {
                    const { label } = parseMarkdownLink(content);
                    ({ attrs: additionalAttrs, node: cellNode } = addContent(label, construct.sharedStrings));
                }
                else if (content && content !== "") {
                    const isTableHeader = isCellTableHeader(c, r, sheet);
                    const isTableTotal = isCellTableTotal(c, r, sheet);
                    const isPlainText = !!(formatId && isTextFormat(data.formats[formatId]));
                    ({ attrs: additionalAttrs, node: cellNode } = addContent(content, construct.sharedStrings, isTableHeader || isTableTotal || isPlainText));
                }
                attributes.push(...additionalAttrs);
                // prettier-ignore
                cellNodes.push(escapeXml /*xml*/ `<c ${formatAttributes(attributes)}>
  ${cellNode}
</c>`);
            }
        }
        if (cellNodes.length ||
            row.size !== DEFAULT_CELL_HEIGHT ||
            row.isHidden ||
            row.outlineLevel ||
            row.collapsed) {
            rowNodes.push(escapeXml /*xml*/ `
        <row ${formatAttributes(rowAttrs)}>
          ${joinXmlNodes(cellNodes)}
        </row>
      `);
        }
    }
    return escapeXml /*xml*/ `
    <sheetData>
      ${joinXmlNodes(rowNodes)}
    </sheetData>
  `;
}
function isCellTableHeader(col, row, sheet) {
    return sheet.tables.some((table) => {
        const zone = toZone(table.range);
        const headerZone = { ...zone, bottom: zone.top };
        return isInside(col, row, headerZone);
    });
}
function isCellTableTotal(col, row, sheet) {
    return sheet.tables.some((table) => {
        if (!table.config.totalRow) {
            return false;
        }
        const zone = toZone(table.range);
        const totalZone = { ...zone, top: zone.bottom };
        return isInside(col, row, totalZone);
    });
}
function addHyperlinks(construct, data, sheetIndex) {
    const sheet = data.sheets[sheetIndex];
    const cells = sheet.cells;
    const linkNodes = [];
    for (const xc in cells) {
        const content = cells[xc];
        if (content && isMarkdownLink(content)) {
            const { label, url } = parseMarkdownLink(content);
            if (isSheetUrl(url)) {
                const sheetId = parseSheetUrl(url);
                const sheet = data.sheets.find((sheet) => sheet.id === sheetId);
                const position = sheet ? `${sheet.name}!A1` : CellErrorType.InvalidReference;
                const hyperlinkAttributes = [
                    ["display", label],
                    ["location", position],
                    ["ref", xc],
                ];
                linkNodes.push(escapeXml /*xml*/ `
          <hyperlink ${formatAttributes(hyperlinkAttributes)}/>
        `);
            }
            else {
                const linkRelId = addRelsToFile(construct.relsFiles, `xl/worksheets/_rels/sheet${sheetIndex}.xml.rels`, {
                    target: withHttps(url),
                    type: XLSX_RELATION_TYPE.hyperlink,
                    targetMode: "External",
                });
                const hyperlinkAttributes = [
                    ["r:id", linkRelId],
                    ["ref", xc],
                ];
                linkNodes.push(escapeXml /*xml*/ `
          <hyperlink ${formatAttributes(hyperlinkAttributes)}/>
        `);
            }
        }
    }
    if (!linkNodes.length) {
        return escapeXml ``;
    }
    return escapeXml /*xml*/ `
    <hyperlinks>
      ${joinXmlNodes(linkNodes)}
    </hyperlinks>
  `;
}
function addMerges(merges) {
    if (merges.length) {
        const mergeNodes = merges.map((merge) => escapeXml /*xml*/ `<mergeCell ref="${merge}" />`);
        return escapeXml /*xml*/ `
      <mergeCells count="${merges.length}">
        ${joinXmlNodes(mergeNodes)}
      </mergeCells>
    `;
    }
    else
        return escapeXml ``;
}
function addSheetViews(sheet) {
    const panes = sheet.panes;
    let splitPanes = escapeXml /*xml*/ ``;
    if (panes && (panes.xSplit || panes.ySplit)) {
        const xc = toXC(panes.xSplit, panes.ySplit);
        //workbookViewId should be defined in the workbook file but it seems like Excel has a default behaviour.
        const xSplit = panes.xSplit ? escapeXml `xSplit="${panes.xSplit}"` : "";
        const ySplit = panes.ySplit ? escapeXml `ySplit="${panes.ySplit}"` : "";
        const topRight = panes.xSplit ? escapeXml `<selection pane="topRight"/>` : "";
        const bottomLeft = panes.ySplit ? escapeXml `<selection pane="bottomLeft"/>` : "";
        const bottomRight = panes.xSplit && panes.ySplit ? escapeXml `<selection pane="bottomRight"/>` : "";
        splitPanes = escapeXml /*xml*/ `
    <pane
      ${xSplit}
      ${ySplit}
      topLeftCell="${xc}"
      activePane="${panes.xSplit ? (panes.ySplit ? "bottomRight" : "topRight") : "bottomLeft"}"
      state="frozen"/>
      ${topRight}
      ${bottomLeft}
      ${bottomRight}
    `;
    }
    const sheetViewAttrs = [
        ["showGridLines", sheet.areGridLinesVisible ? 1 : 0],
        ["workbookViewId", 0],
    ];
    return escapeXml /*xml*/ `
      <sheetViews>
        <sheetView ${formatAttributes(sheetViewAttrs)}>
          ${splitPanes}
        </sheetView>
      </sheetViews>
    `;
}
function addSheetProperties(sheet) {
    if (!sheet.color) {
        return "";
    }
    return escapeXml /*xml*/ `
      <sheetPr>
        <tabColor ${formatAttributes([["rgb", toXlsxHexColor(sheet.color)]])} />
      </sheetPr>
    `;
}

/**
 * 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) {
    data = fixLengthySheetNames(data);
    data = purgeSingleRowTables(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.tables.length;
        // Figures and Charts
        let drawingNode = escapeXml ``;
        const drawingRelIds = [];
        for (const chart of sheet.charts) {
            const xlsxChartId = convertChartId(chart.id, construct);
            const chartRelId = addRelsToFile(construct.relsFiles, `xl/drawings/_rels/drawing${sheetIndex}.xml.rels`, {
                target: `../charts/chart${xlsxChartId}.xml`,
                type: XLSX_RELATION_TYPE.chart,
            });
            drawingRelIds.push(chartRelId);
            files.push(createXMLFile(createChart(chart, sheetIndex, data), `xl/charts/chart${xlsxChartId}.xml`, "chart"));
        }
        for (const image of sheet.images) {
            const mimeType = image.data.mimetype;
            if (mimeType === undefined)
                continue;
            const extension = IMAGE_MIMETYPE_TO_EXTENSION_MAPPING[mimeType];
            // only support exporting images with mimetypes specified in the mapping
            if (extension === undefined)
                continue;
            const xlsxImageId = convertImageId(image.id);
            const imageFileName = `image${xlsxImageId}.${extension}`;
            const imageRelId = addRelsToFile(construct.relsFiles, `xl/drawings/_rels/drawing${sheetIndex}.xml.rels`, {
                target: `../media/${imageFileName}`,
                type: XLSX_RELATION_TYPE.image,
            });
            drawingRelIds.push(imageRelId);
            files.push({
                path: `xl/media/${imageFileName}`,
                imageSrc: image.data.path,
            });
        }
        const drawings = [...sheet.charts, ...sheet.images];
        if (drawings.length) {
            const drawingRelId = addRelsToFile(construct.relsFiles, `xl/worksheets/_rels/sheet${sheetIndex}.xml.rels`, {
                target: `../drawings/drawing${sheetIndex}.xml`,
                type: XLSX_RELATION_TYPE.drawing,
            });
            files.push(createXMLFile(createDrawing(drawingRelIds, sheet, drawings, construct), `xl/drawings/drawing${sheetIndex}.xml`, "drawing"));
            drawingNode = escapeXml /*xml*/ `<drawing r:id="${drawingRelId}" />`;
        }
        const sheetXml = escapeXml /*xml*/ `
      <worksheet ${formatAttributes(namespaces)}>
        ${addSheetProperties(sheet)}
        ${addSheetViews(sheet)}
        <sheetFormatPr ${formatAttributes(sheetFormatAttributes)} />
        ${addColumns(sheet.cols)}
        ${addRows(construct, data, sheet)}
        ${addMerges(sheet.merges)}
        ${joinXmlNodes(addConditionalFormatting(construct.dxfs, sheet.conditionalFormats))}
        ${joinXmlNodes(addDataValidationRules(sheet.dataValidationRules))}
        ${addHyperlinks(construct, data, sheetIndex)}
        ${drawingNode}
        ${tablesNode}
      </worksheet>
    `;
        files.push(createXMLFile(parseXML(sheetXml), `xl/worksheets/sheet${sheetIndex}.xml`, "sheet"));
    }
    const sheetMetadataXml = escapeXml /*xml*/ `
    <metadata xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:xda="http://schemas.microsoft.com/office/spreadsheetml/2017/dynamicarray">
        <metadataTypes count="1">
            <metadataType name="XLDAPR" minSupportedVersion="120000" copy="1" pasteAll="1"
                pasteValues="1" merge="1" splitFirst="1" rowColShift="1" clearFormats="1"
                clearComments="1" assign="1" coerce="1" cellMeta="1" />
        </metadataTypes>
        <futureMetadata name="XLDAPR" count="1">
            <bk>
                <extLst>
                    <ext uri="{${ARRAY_FORMULA_URI}}">
                        <xda:dynamicArrayProperties fDynamic="1" fCollapsed="0" />
                    </ext>
                </extLst>
            </bk>
        </futureMetadata>
        <cellMetadata count="1">
            <bk>
                <rc t="1" v="0" />
            </bk>
        </cellMetadata>
    </metadata>
  `;
    files.push(createXMLFile(parseXML(sheetMetadataXml), "xl/metadata.xml", "metadata"));
    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",
    });
    addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
        type: XLSX_RELATION_TYPE.metadata,
        target: "metadata.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.tables.length)
        return new XMLString("");
    const sheetRelFile = `xl/worksheets/_rels/sheet${sheetId}.xml.rels`;
    const tableParts = [];
    for (const table of sheetData.tables) {
        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.tables.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) => {
        if (string.trim() !== string) {
            return escapeXml /*xml*/ `<si><t xml:space="preserve">${string}</t></si>`;
        }
        return escapeXml /*xml*/ `<si><t>${string}</t></si>`;
    });
    const xml = escapeXml /*xml*/ `
    <sst ${formatAttributes(namespaces)}>
      ${joinXmlNodes(stringNodes)}
    </sst>
  `;
    return createXMLFile(parseXML(xml), "xl/sharedStrings.xml", "sharedStrings");
}
function createRelsFiles(relsFiles) {
    const XMLRelsFiles = [];
    for (const relFile of relsFiles) {
        const relationNodes = [];
        for (const rel of relFile.rels) {
            const attributes = [
                ["Id", rel.id],
                ["Target", rel.target],
                ["Type", rel.type],
            ];
            if (rel.targetMode) {
                attributes.push(["TargetMode", rel.targetMode]);
            }
            relationNodes.push(escapeXml /*xml*/ `
        <Relationship ${formatAttributes(attributes)} />
      `);
        }
        const xml = escapeXml /*xml*/ `
      <Relationships xmlns="${NAMESPACE["Relationships"]}">
        ${joinXmlNodes(relationNodes)}
      </Relationships>
    `;
        XMLRelsFiles.push(createXMLFile(parseXML(xml), relFile.path));
    }
    return XMLRelsFiles;
}
function createContentTypes(files) {
    const overrideNodes = [];
    // hard-code supported image mimetypes
    const imageDefaultNodes = Object.entries(IMAGE_MIMETYPE_TO_EXTENSION_MAPPING).map(([mimetype, extension]) => createDefaultXMLElement(extension, mimetype));
    for (const file of files) {
        if ("contentType" in file && file.contentType) {
            overrideNodes.push(createOverride("/" + file.path, CONTENT_TYPES[file.contentType]));
        }
    }
    const relsAttributes = [
        ["Extension", "rels"],
        ["ContentType", "application/vnd.openxmlformats-package.relationships+xml"],
    ];
    const xmlAttributes = [
        ["Extension", "xml"],
        ["ContentType", "application/xml"],
    ];
    const xml = escapeXml /*xml*/ `
    <Types xmlns="${NAMESPACE["Types"]}">
      ${joinXmlNodes(Object.values(imageDefaultNodes))}
      <Default ${formatAttributes(relsAttributes)} />
      <Default ${formatAttributes(xmlAttributes)} />
      ${joinXmlNodes(overrideNodes)}
    </Types>
  `;
    return createXMLFile(parseXML(xml), "[Content_Types].xml");
}
function createRelRoot() {
    const attributes = [
        ["Id", "rId1"],
        ["Type", XLSX_RELATION_TYPE.document],
        ["Target", "xl/workbook.xml"],
    ];
    const xml = escapeXml /*xml*/ `
    <Relationships xmlns="${NAMESPACE["Relationships"]}">
      <Relationship ${formatAttributes(attributes)} />
    </Relationships>
  `;
    return createXMLFile(parseXML(xml), "_rels/.rels");
}
/**
 * Excel sheet names are maximum 31 characters while o-spreadsheet do not have this limit.
 * This method converts the sheet names to be within the 31 characters limit.
 * The cells/charts referencing this sheet will be updated accordingly.
 */
function fixLengthySheetNames(data) {
    const nameMapping = {};
    const newNames = [];
    for (const sheet of data.sheets) {
        let newName = sheet.name.slice(0, 31);
        newName = getUniqueText(newName, newNames, {
            compute: (name, i) => name.slice(0, 31 - String(i).length) + i,
        });
        newNames.push(newName);
        if (newName !== sheet.name) {
            nameMapping[sheet.name] = newName;
            sheet.name = newName;
        }
    }
    if (!Object.keys(nameMapping).length) {
        return data;
    }
    const sheetWithNewNames = Object.keys(nameMapping).sort((a, b) => b.length - a.length);
    let stringifiedData = JSON.stringify(data);
    for (const sheetName of sheetWithNewNames) {
        const regex = new RegExp(`'?${escapeRegExp(sheetName)}'?!`, "g");
        stringifiedData = stringifiedData.replaceAll(regex, (match) => {
            const newName = nameMapping[sheetName];
            return match.replace(sheetName, newName);
        });
    }
    return JSON.parse(stringifiedData);
}
/** Excel files do not support tables with a single row the defined range
 * Since those tables are not really useful (no filtering/limited styling)
 * This function filters out all tables with a single row.
 */
function purgeSingleRowTables(data) {
    for (const sheet of data.sheets) {
        sheet.tables = sheet.tables.filter((table) => zoneToDimension(toZone(table.range)).numberOfRows > 1);
    }
    return data;
}

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

/**
 * We export here all entities that needs to be accessed publicly by Odoo.
 *
 * Note that the __info__ key is actually completed by the build process (see
 * the rollup.config.js file)
 */
const __info__ = {};
const SPREADSHEET_DIMENSIONS = {
    MIN_ROW_HEIGHT,
    MIN_COL_WIDTH,
    HEADER_HEIGHT,
    HEADER_WIDTH,
    DESKTOP_BOTTOMBAR_HEIGHT,
    DEFAULT_CELL_WIDTH,
    DEFAULT_CELL_HEIGHT,
    SCROLLBAR_WIDTH,
};
const registries = {
    autoCompleteProviders,
    autofillModifiersRegistry,
    autofillRulesRegistry,
    cellMenuRegistry,
    colMenuRegistry,
    errorTypes,
    linkMenuRegistry,
    functionRegistry,
    featurePluginRegistry,
    iconsOnCellRegistry,
    statefulUIPluginRegistry,
    coreViewsPluginRegistry,
    corePluginRegistry,
    rowMenuRegistry,
    sidePanelRegistry,
    figureRegistry,
    chartSidePanelComponentRegistry,
    chartComponentRegistry,
    chartRegistry,
    chartSubtypeRegistry,
    topbarMenuRegistry,
    topbarComponentRegistry,
    clickableCellRegistry,
    otRegistry,
    inverseCommandRegistry,
    urlRegistry,
    cellPopoverRegistry,
    numberFormatMenuRegistry,
    repeatLocalCommandTransformRegistry,
    repeatCommandTransformRegistry,
    clipboardHandlersRegistries,
    pivotRegistry,
    pivotTimeAdapterRegistry,
    pivotSidePanelRegistry,
    pivotNormalizationValueRegistry,
    supportedPivotPositionalFormulaRegistry,
    pivotToFunctionValueRegistry,
    migrationStepRegistry,
    chartJsExtensionRegistry,
};
const helpers = {
    arg,
    isEvaluationError,
    toBoolean,
    toJsDate,
    toNumber,
    toString,
    toNormalizedPivotValue,
    toFunctionPivotValue,
    toXC,
    toZone,
    toUnboundedZone,
    toCartesian,
    numberToLetters,
    lettersToNumber,
    UuidGenerator,
    formatValue,
    createCurrencyFormat,
    ColorGenerator,
    computeTextWidth,
    createEmptyWorkbookData,
    createEmptySheet,
    createEmptyExcelSheet,
    rgbaToHex,
    colorToRGBA,
    positionToZone,
    isDefined,
    isMatrix,
    lazy,
    genericRepeat,
    createAction,
    createActions,
    transformRangeData,
    deepEquals,
    overlap,
    union,
    isInside,
    deepCopy,
    expandZoneOnInsertion,
    reduceZoneOnDeletion,
    unquote,
    getMaxObjectId,
    getFunctionsFromTokens,
    getFirstPivotFunction,
    getNumberOfPivotFunctions,
    parseDimension,
    isDateOrDatetimeField,
    makeFieldProposal,
    insertTokenAfterArgSeparator,
    insertTokenAfterLeftParenthesis,
    mergeContiguousZones,
    getPivotHighlights,
    pivotTimeAdapter,
    UNDO_REDO_PIVOT_COMMANDS,
    createPivotFormula,
    areDomainArgsFieldsValid,
    splitReference,
    sanitizeSheetName,
    getUniqueText,
    isNumber,
    isDateTime,
    createCustomFields,
};
const links = {
    isMarkdownLink,
    parseMarkdownLink,
    markdownLink,
    openLink,
    urlRepresentation,
};
const components = {
    Checkbox,
    Section,
    RoundColorPicker,
    ChartDataSeries,
    ChartErrorSection,
    ChartLabelRange,
    ChartTitle,
    ChartPanel,
    ChartFigure,
    ChartJsComponent,
    ClickableCellSortIcon,
    ZoomableChartJsComponent,
    Grid,
    GridOverlay,
    ScorecardChart,
    GaugeChartComponent,
    LineConfigPanel,
    BarConfigPanel,
    PieChartDesignPanel,
    GenericChartConfigPanel,
    ChartWithAxisDesignPanel,
    GenericZoomableChartDesignPanel,
    LineChartDesignPanel,
    GaugeChartConfigPanel,
    GaugeChartDesignPanel,
    ScorecardChartConfigPanel,
    ScorecardChartDesignPanel,
    GeoChartDesignPanel,
    RadarChartDesignPanel,
    WaterfallChartDesignPanel,
    ComboChartDesignPanel,
    FunnelChartDesignPanel,
    SunburstChartDesignPanel,
    TreeMapChartDesignPanel,
    ChartTypePicker,
    FigureComponent,
    MenuPopover,
    Popover,
    SelectionInput,
    ValidationMessages,
    AddDimensionButton,
    PivotDimensionGranularity,
    PivotDimensionOrder,
    PivotDimension,
    PivotLayoutConfigurator,
    PivotHTMLRenderer,
    PivotDeferUpdate,
    PivotTitleSection,
    CogWheelMenu,
    TextInput,
    SidePanelCollapsible,
    RadioSelection,
    GeoChartRegionSelectSection,
    ChartDashboardMenu,
    FullScreenFigure,
};
const hooks = {
    useDragAndDropListItems,
    useHighlights,
    useHighlightsOnHover,
};
const stores = {
    useStoreProvider,
    DependencyContainer,
    CellPopoverStore,
    ComposerFocusStore,
    CellComposerStore,
    FindAndReplaceStore,
    HighlightStore,
    DelayedHoveredCellStore,
    HoveredTableStore,
    ModelStore,
    NotificationStore,
    RendererStore,
    SelectionInputStore,
    SpreadsheetStore,
    useStore,
    useLocalStore,
    SidePanelStore,
    PivotSidePanelStore,
    PivotMeasureDisplayPanelStore,
    ClientFocusStore,
    GridRenderer,
};
function addFunction(functionName, functionDescription) {
    functionRegistry.add(functionName, functionDescription);
    return {
        addFunction: (fName, fDescription) => addFunction(fName, fDescription),
    };
}
const constants = {
    DEFAULT_LOCALE,
    HIGHLIGHT_COLOR,
    PIVOT_TABLE_CONFIG,
    ChartTerms,
    FIGURE_ID_SPLITTER,
    GRID_ICON_EDGE_LENGTH,
    GRID_ICON_MARGIN,
};
const chartHelpers = { ...CHART_HELPERS, ...CHART_RUNTIME_HELPERS };

Object.assign(__exports, { AbstractCellClipboardHandler, AbstractChart, AbstractFigureClipboardHandler, CellErrorType, ClientDisconnectedError, CommandResult, CorePlugin, CoreViewPlugin, DispatchResult, EvaluationError, LocalTransportService, Model, PivotRuntimeDefinition, Registry, Revision, SPREADSHEET_DIMENSIONS, Spreadsheet, SpreadsheetPivotTable, UIPlugin, __info__, addFunction, addRenderingLayer, astToFormula, chartHelpers, compile, compileTokens, components, constants, convertAstNodes, coreTypes, findCellInNewZone, functionCache, getCaretDownSvg, getCaretUpSvg, helpers, hooks, invalidateCFEvaluationCommands, invalidateChartEvaluationCommands, invalidateDependenciesCommands, invalidateEvaluationCommands, iterateAstNodes, links, load, parse, parseTokens, readonlyAllowedCommands, registries, setDefaultSheetViewSize, setTranslationMethod, stores, tokenColors, tokenize });


__info__.version = "19.0.24";
__info__.date = "2026-03-06T07:17:55.361Z";
__info__.hash = "7e8f4d894";
//# sourceMappingURL=o_spreadsheet.esm.js.map
return __exports;
});
