/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert, oob } from "@fluidframework/core-utils/internal";
import { UsageError } from "@fluidframework/telemetry-utils/internal";
import { Tree, TreeAlpha } from "./shared-tree/index.js";
import { TreeArrayNode, SchemaFactory, isArrayNodeSchema, } from "./simple-tree/index.js";
// Future improvement TODOs:
// - Omit `cells` property from Row insertion type.
// - Record-like type parameters / input parameters?
// - Omit `props` properties from Row and Column schemas when not provided?
// Longer-term work:
// - Add constraint APIs to make it possible to avoid situations that could yield "orphaned" cells.
/**
 * The sub-scope applied to user-provided {@link SchemaFactory}s by table schema factories.
 */
const tableSchemaFactorySubScope = "table";
/**
 * Gets the table containing the provided row/column node, if the node is part of a table.
 * @remarks Assumes that the table is the grandparent of the row/column node.
 */
function getParentTable(rowOrColumnNode) {
    const rowListNode = Tree.parent(rowOrColumnNode);
    if (rowListNode === undefined || !isArrayNodeSchema(Tree.schema(rowListNode))) {
        return undefined;
    }
    const tableNode = Tree.parent(rowListNode);
    if (tableNode === undefined || !isTableNode(tableNode)) {
        return undefined;
    }
    return tableNode;
}
/**
 * A private symbol put on table schema to help identify them.
 */
const tableSchemaSymbol = Symbol("tableNode");
/**
 * Type-guard to determine if a node is a table node.
 * @remarks Uses {@link tableSchemaSymbol} to identify table nodes.
 */
function isTableNode(node) {
    return tableSchemaSymbol in Tree.schema(node);
}
/**
 * Not intended for use outside of this package.
 *
 * @privateRemarks
 * This namespace is a collection of internal system types relate to {@link TableSchema}.
 * This namespace should be strictly type-exported by the package.
 * All members should be tagged with `@system`.
 *
 * @system @alpha
 */
export var System_TableSchema;
(function (System_TableSchema) {
    /**
     * Factory for creating column schema.
     * @system @alpha
     */
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -- Return type is too complex to be reasonable to specify
    function createColumnSchema(inputSchemaFactory, cellSchema, propsSchema) {
        const schemaFactory = inputSchemaFactory.scopedFactory(tableSchemaFactorySubScope);
        // Note: `columnFields` is broken into two parts to work around a TypeScript bug
        // that results in broken `.d.ts` output.
        // See definition of `ColumnInsertableType` below.
        const columnFieldsBuiltInParts = {
            id: schemaFactory.identifier,
        };
        const columnFieldsPropsPart = {
            props: propsSchema,
        };
        /**
         * {@link Column} fields.
         *
         * @remarks
         * Extracted for re-use in returned type signature defined later in this function.
         * The implicit typing is intentional.
         *
         * Note: ideally we would add a satisfies clause here to ensure that this satisfies
         * `Record<string, ImplicitFieldSchema>`, but doing so causes TypeScript to prematurely and incorrectly evaluate the type of `propsSchema`.
         * Likely related to the following issue: https://github.com/microsoft/TypeScript/issues/52394
         */
        const columnFields = {
            ...columnFieldsBuiltInParts,
            ...columnFieldsPropsPart,
        }; // satisfies Record<string, ImplicitFieldSchema>;
        /**
         * A column in a table.
         */
        class Column extends schemaFactory.objectAlpha("Column", columnFields, {
            // Will make it easier to evolve this schema in the future.
            allowUnknownOptionalFields: true,
        }) {
            getCells() {
                const tableNode = getParentTable(this);
                if (tableNode === undefined) {
                    throw new UsageError(`Column with ID "${this.id}" is not contained in a table.`);
                }
                const result = [];
                for (const row of tableNode.rows) {
                    const cell = row.getCell(this.id);
                    if (cell !== undefined) {
                        if (!Tree.is(cell, cellSchema)) {
                            throw new UsageError("Parent table contains a cell with incompatible with this column.");
                        }
                        result.push({ rowId: row.id, cell: cell });
                    }
                }
                return result;
            }
        }
        // Returning SingletonSchema without a type conversion results in TypeScript generating something like `readonly "__#124291@#brand": unknown;`
        // for the private brand field of TreeNode.
        // This numeric id doesn't seem to be stable over incremental builds, and thus causes diffs in the API extractor reports.
        // This is avoided by doing this type conversion.
        // The conversion is done via assignment instead of `as` to get stronger type safety.
        const ColumnSchemaType = Column;
        return ColumnSchemaType;
    }
    System_TableSchema.createColumnSchema = createColumnSchema;
    /**
     * Factory for creating row schema.
     * @sealed @alpha
     */
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -- Return type is too complex to be reasonable to specify
    function createRowSchema(inputSchemaFactory, cellSchema, propsSchema) {
        const schemaFactory = inputSchemaFactory.scopedFactory(tableSchemaFactorySubScope);
        // Note: `rowFields` is broken into two parts to work around a TypeScript bug
        // that results in broken `.d.ts` output.
        // See definition of `RowInsertableType` below.
        const rowFieldsBuiltInParts = {
            id: schemaFactory.identifier,
            /**
             * The cells of the table row, keyed by column ID.
             * @remarks
             * The table row models its cells as a record, where each key is the ID of the column it belongs to. The choice of record (as opposed to a map) is intended to make interop with common table rendering libraries in TypeScript/JavaScript easier.
             */
            cells: schemaFactory.required(schemaFactory.record("Row.cells", cellSchema), {
                metadata: {
                    description: "The cells of the table row, keyed by column ID.",
                },
            }),
        };
        const rowFieldsPropsPart = {
            props: propsSchema,
        };
        /**
         * {@link Row} fields.
         * @remarks Extracted for re-use in returned type signature defined later in this function.
         * The implicit typing is intentional.
         * Note: ideally we would add a satisfies clause here to ensure that this satisfies
         * `Record<string, ImplicitFieldSchema>`, but doing so causes TypeScript to prematurely and incorrectly evaluate the type of `propsSchema`.
         * Likely related to the following issue: https://github.com/microsoft/TypeScript/issues/52394
         */
        const rowFields = {
            ...rowFieldsBuiltInParts,
            ...rowFieldsPropsPart,
        }; // satisfies Record<string, ImplicitFieldSchema>;
        /**
         * The Row schema - this is a map of Cells where the key is the column id
         */
        class Row extends schemaFactory.objectAlpha("Row", rowFields, {
            // Will make it easier to evolve this schema in the future.
            allowUnknownOptionalFields: true,
        }) {
            getCell(columnOrId) {
                const columnId = typeof columnOrId === "string" ? columnOrId : columnOrId.id;
                // Unlike most objects, RecordNodes don't have the default inherited object properties, so this is safe
                return this.cells[columnId];
            }
            setCell(columnOrId, value) {
                // TODO: throw if column does not exist in the owning table.
                if (value === undefined) {
                    this.removeCell(columnOrId);
                }
                else {
                    const columnId = typeof columnOrId === "string" ? columnOrId : columnOrId.id;
                    // TypeScript is unable to narrow the types correctly here, hence the casts.
                    // See: https://github.com/microsoft/TypeScript/issues/52144
                    this.cells[columnId] = TreeAlpha.create(cellSchema, value);
                }
            }
            removeCell(columnOrId) {
                // TODO: throw if column does not exist in the owning table.
                const columnId = typeof columnOrId === "string" ? columnOrId : columnOrId.id;
                const cell = this.getCell(columnId);
                if (cell === undefined) {
                    return undefined;
                }
                // eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- The record's values are non-optional, so setting `undefined` as a means to remove the cell is not supported.
                delete this.cells[columnId];
                return cell;
            }
            getCells() {
                const result = [];
                for (const [columnId, cell] of Object.entries(this.cells)) {
                    if (cell !== undefined) {
                        result.push({
                            columnId,
                            cell,
                        });
                    }
                }
                return result;
            }
        }
        // Returning SingletonSchema without a type conversion results in TypeScript generating something like `readonly "__#124291@#brand": unknown;`
        // for the private brand field of TreeNode.
        // This numeric id doesn't seem to be stable over incremental builds, and thus causes diffs in the API extractor reports.
        // This is avoided by doing this type conversion.
        // The conversion is done via assignment instead of `as` to get stronger type safety.
        const RowSchemaType = Row;
        return RowSchemaType;
    }
    System_TableSchema.createRowSchema = createRowSchema;
    /**
     * Factory for creating table schema.
     * @system @alpha
     */
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -- Return type is too complex to be reasonable to specify
    function createTableSchema(inputSchemaFactory, _cellSchema, columnSchema, rowSchema) {
        const schemaFactory = inputSchemaFactory.scopedFactory(tableSchemaFactorySubScope);
        /**
         * {@link Table} fields.
         * @remarks Extracted for re-use in returned type signature defined later in this function.
         * The implicit typing is intentional.
         */
        const tableFields = {
            rows: schemaFactory.array("Table.rows", rowSchema),
            columns: schemaFactory.array("Table.columns", columnSchema),
        };
        /**
         * The Table schema
         */
        class Table extends schemaFactory.objectAlpha("Table", tableFields, {
            // Will make it easier to evolve this schema in the future.
            allowUnknownOptionalFields: true,
        }) {
            static empty() {
                return new this({ columns: [], rows: [] });
            }
            getColumn(id) {
                // TypeScript is unable to narrow the types correctly here, hence the casts.
                // See: https://github.com/microsoft/TypeScript/issues/52144
                return this.columns.find((column) => column.id === id);
            }
            getRow(id) {
                // TypeScript is unable to narrow the types correctly here, hence the casts.
                // See: https://github.com/microsoft/TypeScript/issues/52144
                return this.rows.find((_row) => _row.id === id);
            }
            getCell(key) {
                const { column: columnOrId, row: rowOrId } = key;
                const row = this._getRow(rowOrId);
                if (row === undefined) {
                    return undefined;
                }
                const column = this._getColumn(columnOrId);
                if (column === undefined) {
                    return undefined;
                }
                return row.getCell(column);
            }
            insertColumn({ column, index, }) {
                const inserted = this.insertColumns({
                    columns: [column],
                    index,
                });
                return inserted[0] ?? oob();
            }
            insertColumns({ columns, index, }) {
                // Ensure index is valid
                if (index !== undefined) {
                    Table.validateInsertionIndex(index, this.columns);
                }
                // TypeScript is unable to narrow the column type correctly here, hence the casts below.
                // See: https://github.com/microsoft/TypeScript/issues/52144
                if (index === undefined) {
                    // eslint-disable-next-line @typescript-eslint/no-explicit-any
                    this.columns.insertAtEnd(TreeArrayNode.spread(columns));
                }
                else {
                    // eslint-disable-next-line @typescript-eslint/no-explicit-any
                    this.columns.insertAt(index, TreeArrayNode.spread(columns));
                }
                // Inserting the input nodes into the tree hydrates them, making them usable as nodes.
                return columns;
            }
            insertRow({ row, index, }) {
                const inserted = this.insertRows({
                    rows: [row],
                    index,
                });
                return inserted[0] ?? oob();
            }
            insertRows({ index, rows, }) {
                // #region Input validation
                // Ensure index is valid
                if (index !== undefined) {
                    Table.validateInsertionIndex(index, this.rows);
                }
                // Note: TypeScript is unable to narrow the type of the row type correctly here, hence the casts below.
                // See: https://github.com/microsoft/TypeScript/issues/52144
                for (const newRow of rows) {
                    // If the row contains cells, verify that the table contains the columns for those cells.
                    // Note: we intentionally hide `cells` on `IRow` to avoid leaking the internal data representation as much as possible, so we have to cast here.
                    // eslint-disable-next-line @typescript-eslint/no-explicit-any
                    if (newRow.cells !== undefined) {
                        // eslint-disable-next-line @typescript-eslint/no-explicit-any
                        const keys = Object.keys(newRow.cells);
                        for (const key of keys) {
                            if (!this.containsColumnWithId(key)) {
                                throw new UsageError(`Attempted to insert row a cell under column ID "${key}", but the table does not contain a column with that ID.`);
                            }
                        }
                    }
                }
                // #endregion
                // TypeScript is unable to narrow the row type correctly here, hence the casts below.
                // See: https://github.com/microsoft/TypeScript/issues/52144
                if (index === undefined) {
                    // eslint-disable-next-line @typescript-eslint/no-explicit-any
                    this.rows.insertAtEnd(TreeArrayNode.spread(rows));
                }
                else {
                    // eslint-disable-next-line @typescript-eslint/no-explicit-any
                    this.rows.insertAt(index, TreeArrayNode.spread(rows));
                }
                // Inserting the input nodes into the tree hydrates them, making them usable as nodes.
                return rows;
            }
            setCell({ key, cell, }) {
                const { column: columnOrId, row: rowOrId } = key;
                const row = this._getRow(rowOrId);
                if (row === undefined) {
                    const rowId = this._getRowId(rowOrId);
                    throw new UsageError(`No row with ID "${rowId}" exists in the table.`);
                }
                const column = this._getColumn(columnOrId);
                if (column === undefined) {
                    const columnId = this._getColumnId(columnOrId);
                    throw new UsageError(`No column with ID "${columnId}" exists in the table.`);
                }
                row.setCell(column, cell);
            }
            removeColumns(columns) {
                // If there are no columns to remove, do nothing
                if (columns.length === 0) {
                    return [];
                }
                // If there is only one column to remove, remove it (and don't incur cost of transaction)
                if (columns.length === 1) {
                    const removedColumn = this.removeColumn(columns[0] ?? oob());
                    return [removedColumn];
                }
                // If there are multiple columns to remove, remove them in a transaction.
                const removedColumns = [];
                Tree.runTransaction(this, () => {
                    // Note, throwing an error within a transaction will abort the entire transaction.
                    // So if we throw an error here for any row, no columns will be removed.
                    for (const columnToRemove of columns) {
                        const removedRow = this.removeColumn(columnToRemove);
                        removedColumns.push(removedRow);
                    }
                });
                return removedColumns;
            }
            removeColumn(columnOrId) {
                const column = this._getColumn(columnOrId);
                const index = column === undefined ? -1 : this.columns.indexOf(column);
                if (index === -1) {
                    const columnId = this._getColumnId(columnOrId);
                    throw new UsageError(`Specified column with ID "${columnId}" does not exist in the table.`);
                }
                assert(column !== undefined, 0xc10 /* column should not be undefined */);
                Tree.runTransaction(this, () => {
                    // Remove the corresponding cell from all rows.
                    for (const row of this.rows) {
                        // TypeScript is unable to narrow the row type correctly here, hence the cast.
                        // See: https://github.com/microsoft/TypeScript/issues/52144
                        row.removeCell(column);
                    }
                    this.columns.removeAt(index);
                });
                return column;
            }
            removeAllColumns() {
                // TypeScript is unable to narrow the row type correctly here, hence the cast.
                // See: https://github.com/microsoft/TypeScript/issues/52144
                return this.removeColumns(this.columns);
            }
            removeRows(rows) {
                // If there are no rows to remove, do nothing
                if (rows.length === 0) {
                    return [];
                }
                // If there is only one row to remove, remove it (and don't incur cost of transaction)
                if (rows.length === 1) {
                    const removedRow = this.removeRow(rows[0] ?? oob());
                    return [removedRow];
                }
                // If there are multiple rows to remove, remove them in a transaction.
                const removedRows = [];
                Tree.runTransaction(this, () => {
                    // Note, throwing an error within a transaction will abort the entire transaction.
                    // So if we throw an error here for any row, no rows will be removed.
                    for (const rowToRemove of rows) {
                        const removedRow = this.removeRow(rowToRemove);
                        removedRows.push(removedRow);
                    }
                });
                return removedRows;
            }
            removeRow(rowOrId) {
                const rowToRemove = this._getRow(rowOrId);
                const index = rowToRemove === undefined ? -1 : this.rows.indexOf(rowToRemove);
                // If the row does not exist in the table, throw an error.
                if (index === -1) {
                    const rowId = this._getRowId(rowOrId);
                    throw new UsageError(`Specified row with ID "${rowId}" does not exist in the table.`);
                }
                this.rows.removeAt(index);
                return rowToRemove;
            }
            removeAllRows() {
                // TypeScript is unable to narrow the row type correctly here, hence the cast.
                // See: https://github.com/microsoft/TypeScript/issues/52144
                return this.removeRows(this.rows);
            }
            removeCell(key) {
                const { column: columnOrId, row: rowOrId } = key;
                const row = this._getRow(rowOrId);
                if (row === undefined) {
                    const rowId = this._getRowId(rowOrId);
                    throw new UsageError(`Specified row with ID "${rowId}" does not exist in the table.`);
                }
                const column = this._getColumn(columnOrId);
                if (column === undefined) {
                    const columnId = this._getColumnId(columnOrId);
                    throw new UsageError(`Specified column with ID "${columnId}" does not exist in the table.`);
                }
                const cell = row.getCell(column.id);
                if (cell === undefined) {
                    return undefined;
                }
                row.removeCell(column.id);
                return cell;
            }
            _getColumn(columnOrId) {
                return typeof columnOrId === "string" ? this.getColumn(columnOrId) : columnOrId;
            }
            _getColumnId(columnOrId) {
                return typeof columnOrId === "string" ? columnOrId : columnOrId.id;
            }
            _getRow(rowOrId) {
                return typeof rowOrId === "string" ? this.getRow(rowOrId) : rowOrId;
            }
            _getRowId(rowOrId) {
                return typeof rowOrId === "string" ? rowOrId : rowOrId.id;
            }
            containsColumnWithId(columnId) {
                // TypeScript is unable to narrow the types correctly here, hence the cast.
                // See: https://github.com/microsoft/TypeScript/issues/52144
                return (this.columns.find((column) => column.id === columnId) !== undefined);
            }
            containsRowWithId(rowId) {
                // TypeScript is unable to narrow the types correctly here, hence the cast.
                // See: https://github.com/microsoft/TypeScript/issues/52144
                return (this.rows.find((row) => row.id === rowId) !==
                    undefined);
            }
            /**
             * Ensure that the specified index is a valid location for item insertion in the destination list.
             * @throws Throws a usage error if the destination is invalid.
             */
            static validateInsertionIndex(index, destinationList) {
                if (index < 0) {
                    throw new UsageError("The index must be greater than or equal to 0.");
                }
                if (index > destinationList.length) {
                    throw new UsageError("The index specified for insertion is out of bounds.");
                }
                if (!Number.isInteger(index)) {
                    throw new UsageError("The index must be an integer.");
                }
            }
        }
        // Set a private symbol on the schema class that marks it as having been generated by this factory.
        // Column / Row functionality use this to validate that they are being used in a table.
        // This is effectively a work-around that allows columns and rows to invoke table methods
        // without having to pass the table as a parameter to their construction, which isn't possible.
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        Table[tableSchemaSymbol] = true;
        // Returning SingletonSchema without a type conversion results in TypeScript generating something like `readonly "__#124291@#brand": unknown;`
        // for the private brand field of TreeNode.
        // This numeric id doesn't seem to be stable over incremental builds, and thus causes diffs in the API extractor reports.
        // This is avoided by doing this type conversion.
        // The conversion is done via assignment instead of `as` to get stronger type safety.
        const TableSchemaType = Table;
        // Return the table schema
        return TableSchemaType;
    }
    System_TableSchema.createTableSchema = createTableSchema;
    // #endregion
})(System_TableSchema || (System_TableSchema = {}));
/**
 * Contains types and factories for creating schema to represent dynamic tabular data.
 *
 * @remarks
 *
 * WARNING: These APIs are in preview and are subject to change.
 * Until these APIs have stabilized, it is not recommended to use them in production code.
 * There may be breaking changes to these APIs and their underlying data format.
 * Using these APIs in production code may result in data loss or corruption.
 *
 * The primary APIs for create tabular data schema are:
 *
 * - {@link TableSchema.(table:1)}
 *
 * - {@link TableSchema.(column:1)}
 *
 * - {@link TableSchema.(row:1)}
 *
 * Tables created using these APIs are...
 *
 * - sparse, meaning that cells may be omitted, and new rows are empty by default.
 *
 * - dynamic, meaning that their structure can be modified at runtime.
 * Columns and rows can be inserted, removed, modified, and reordered.
 * Cells can be inserted, removed, and modified.
 *
 * - row-major, meaning that operating on rows (including inserts, removal, moves, and traversal) is more efficient than operating on columns.
 *
 * Column and Row schema created using these APIs are extensible via the `props` field.
 * This allows association of additional properties with column and row nodes.
 *
 * Cells in the table may become "orphaned."
 * That is, it is possible to enter a state where one or more rows contain cells with no corresponding column.
 * To reduce the likelihood of this, you can manually remove corresponding cells when removing columns.
 * Either way, it is possible to enter such a state via the merging of edits.
 * For example: one client might add a row while another concurrently removes a column, orphaning the cell where the column and row intersected.
 *
 * @example Defining a Table schema
 *
 * ```typescript
 * class MyTable extends TableSchema.table({
 * 	schemaFactory,
 * 	cell: schemaFactory.string,
 * }) {}
 *
 * const table = new MyTable({
 * 	columns: [{ id: "column-0" }],
 * 	rows: [{ id: "row-0", cells: { "column-0": "Hello world!" } }],
 * });
 * ```
 *
 * @example Customizing Column and Row schema
 *
 * ```typescript
 * const Cell = schemaFactory.string;
 *
 * class MyColumn extends TableSchema.column({
 * 	schemaFactory,
 * 	cell: Cell,
 * 	props: schemaFactory.object("TableColumnProps", {
 * 		label: schemaFactory.string,
 * 	}),
 * }) {}
 *
 * class MyRow extends TableSchema.row({
 * 	schemaFactory,
 * 	cell: Cell,
 * }) {}
 *
 * class MyTable extends TableSchema.table({
 * 	schemaFactory,
 * 	cell: Cell,
 * 	column: MyColumn,
 * 	row: MyRow,
 * }) {}
 *
 * const table = new MyTable({
 * 	columns: [
 * 		new MyColumn({ props: { label: "Entry" } }),
 * 		new MyColumn({ props: { label: "Date" } }),
 * 		new MyColumn({ props: { label: "Amount" } }),
 * 	],
 * 	rows: [],
 * });
 * ```
 *
 * @example Listening for changes in the table
 *
 * ```typescript
 * // Listen for any changes to the table and its children.
 * // The "treeChanged" event will fire when the associated node or any of its descendants change.
 * Tree.on(table, "treeChanged", () => {
 * 	// Respond to the change.
 * });
 * ```
 *
 * @example Listening for changes to the rows list only
 *
 * ```typescript
 * // Listen for any changes to the list of rows.
 * // The "nodeChanged" event will fire only when the specified node itself changes (i.e., its own properties change).
 * // In this case, the event will fire when a row is added or removed, or the order of the list is changed.
 * // But it won't fire when a row's properties change, or when the row's cells change, etc.
 * Tree.on(table.rows, "nodeChanged", () => {
 * 	// Respond to the change.
 * });
 * ```
 *
 * @privateRemarks
 * The above examples are backed by tests in `tableSchema.spec.ts`.
 * Those tests and these examples should be kept in-sync to ensure that the examples are correct.
 *
 * @alpha
 */
export var TableSchema;
(function (TableSchema) {
    // #region Column
    /**
     * Overload implementation
     */
    function column({ schemaFactory, cell, props = SchemaFactory.optional(SchemaFactory.null), }) {
        return System_TableSchema.createColumnSchema(schemaFactory, cell, props);
    }
    TableSchema.column = column;
    /**
     * Overload implementation
     */
    function row({ schemaFactory, cell, props = SchemaFactory.optional(SchemaFactory.null), }) {
        return System_TableSchema.createRowSchema(schemaFactory, cell, props);
    }
    TableSchema.row = row;
    /**
     * Overload implementation
     */
    function table({ schemaFactory, cell: cellSchema, column: columnSchema = column({
        schemaFactory,
        cell: cellSchema,
    }), row: rowSchema = row({
        schemaFactory,
        cell: cellSchema,
    }), }) {
        return System_TableSchema.createTableSchema(schemaFactory, cellSchema, columnSchema, rowSchema);
    }
    TableSchema.table = table;
    // #endregion
})(TableSchema || (TableSchema = {}));
//# sourceMappingURL=tableSchema.js.map