import _ from 'lodash';
import '../../../../assert/js/assert.js';
import '../../../../globals.js';
// Copyright 2022-2024, University of Colorado Boulder
/**
 * A grid-based layout container.
 *
 * See https://phetsims.github.io/scenery/doc/layout#GridBox for details
 *
 * GridBox-only options:
 *   - rows (see https://phetsims.github.io/scenery/doc/layout#GridBox-rows)
 *   - columns (see https://phetsims.github.io/scenery/doc/layout#GridBox-columns)
 *   - autoRows (see https://phetsims.github.io/scenery/doc/layout#GridBox-autoLines)
 *   - autoColumns (see https://phetsims.github.io/scenery/doc/layout#GridBox-autoLines)
 *   - resize (see https://phetsims.github.io/scenery/doc/layout#GridBox-resize)
 *   - spacing (see https://phetsims.github.io/scenery/doc/layout#GridBox-spacing)
 *   - xSpacing (see https://phetsims.github.io/scenery/doc/layout#GridBox-spacing)
 *   - ySpacing (see https://phetsims.github.io/scenery/doc/layout#GridBox-spacing)
 *   - layoutOrigin (see https://phetsims.github.io/scenery/doc/layout#layoutOrigin)
 *
 * GridBox and layoutOptions options (can be set either in the GridBox itself, or within its child nodes' layoutOptions):
 *   - xAlign (see https://phetsims.github.io/scenery/doc/layout#GridBox-align)
 *   - yAlign (see https://phetsims.github.io/scenery/doc/layout#GridBox-align)
 *   - stretch (see https://phetsims.github.io/scenery/doc/layout#GridBox-stretch)
 *   - xStretch (see https://phetsims.github.io/scenery/doc/layout#GridBox-stretch)
 *   - yStretch (see https://phetsims.github.io/scenery/doc/layout#GridBox-stretch)
 *   - grow (see https://phetsims.github.io/scenery/doc/layout#GridBox-grow)
 *   - xGrow (see https://phetsims.github.io/scenery/doc/layout#GridBox-grow)
 *   - yGrow (see https://phetsims.github.io/scenery/doc/layout#GridBox-grow)
 *   - margin (see https://phetsims.github.io/scenery/doc/layout#GridBox-margins)
 *   - xMargin (see https://phetsims.github.io/scenery/doc/layout#GridBox-margins)
 *   - yMargin (see https://phetsims.github.io/scenery/doc/layout#GridBox-margins)
 *   - leftMargin (see https://phetsims.github.io/scenery/doc/layout#GridBox-margins)
 *   - rightMargin (see https://phetsims.github.io/scenery/doc/layout#GridBox-margins)
 *   - topMargin (see https://phetsims.github.io/scenery/doc/layout#GridBox-margins)
 *   - bottomMargin (see https://phetsims.github.io/scenery/doc/layout#GridBox-margins)
 *   - minContentWidth (see https://phetsims.github.io/scenery/doc/layout#GridBox-minContent)
 *   - minContentHeight (see https://phetsims.github.io/scenery/doc/layout#GridBox-minContent)
 *   - maxContentWidth (see https://phetsims.github.io/scenery/doc/layout#GridBox-maxContent)
 *   - maxContentHeight (see https://phetsims.github.io/scenery/doc/layout#GridBox-maxContent)
 *
 * layoutOptions-only options (can only be set within the child nodes' layoutOptions, NOT available on GridBox):
 *   - x (see https://phetsims.github.io/scenery/doc/layout#GridBox-layoutOptions-location)
 *   - y (see https://phetsims.github.io/scenery/doc/layout#GridBox-layoutOptions-location)
 *   - horizontalSpan (see https://phetsims.github.io/scenery/doc/layout#GridBox-layoutOptions-size)
 *   - verticalSpan (see https://phetsims.github.io/scenery/doc/layout#GridBox-layoutOptions-size)
 *
 * @author Jonathan Olson <jonathan.olson@colorado.edu>
 */
import assertMutuallyExclusiveOptions from '../../../../phet-core/js/assertMutuallyExclusiveOptions.js';
import optionize from '../../../../phet-core/js/optionize.js';
import Orientation from '../../../../phet-core/js/Orientation.js';
import { GRID_CONSTRAINT_OPTION_KEYS, GridCell, GridConstraint, LAYOUT_NODE_OPTION_KEYS, LayoutAlign, LayoutNode, MarginLayoutCell, Node, REQUIRES_BOUNDS_OPTION_KEYS, scenery, SIZABLE_OPTION_KEYS } from '../../imports.js';
// GridBox-specific options that can be passed in the constructor or mutate() call.
const GRIDBOX_OPTION_KEYS = [
    ...LAYOUT_NODE_OPTION_KEYS,
    ...GRID_CONSTRAINT_OPTION_KEYS.filter(key => key !== 'excludeInvisible'),
    'rows',
    'columns',
    'autoRows',
    'autoColumns'
];
export default class GridBox extends LayoutNode {
    constructor(providedOptions) {
        const options = optionize()({
            // Allow dynamic layout by default, see https://github.com/phetsims/joist/issues/608
            excludeInvisibleChildrenFromBounds: true,
            resize: true
        }, providedOptions);
        super();
        this._cellMap = new Map();
        // For handling auto-wrapping features
        this._autoRows = null;
        this._autoColumns = null;
        // So we don't kill performance while setting children with autoRows/autoColumns
        this._autoLockCount = 0;
        this._constraint = new GridConstraint(this, {
            preferredWidthProperty: this.localPreferredWidthProperty,
            preferredHeightProperty: this.localPreferredHeightProperty,
            minimumWidthProperty: this.localMinimumWidthProperty,
            minimumHeightProperty: this.localMinimumHeightProperty,
            layoutOriginProperty: this.layoutOriginProperty,
            excludeInvisible: false // Should be handled by the options mutate below
        });
        this.onChildInserted = this.onGridBoxChildInserted.bind(this);
        this.onChildRemoved = this.onGridBoxChildRemoved.bind(this);
        this.onChildVisibilityToggled = this.updateAllAutoLines.bind(this);
        this.childInsertedEmitter.addListener(this.onChildInserted);
        this.childRemovedEmitter.addListener(this.onChildRemoved);
        const nonBoundsOptions = _.omit(options, REQUIRES_BOUNDS_OPTION_KEYS);
        const boundsOptions = _.pick(options, REQUIRES_BOUNDS_OPTION_KEYS);
        // Before we layout, do non-bounds-related changes (in case we have resize:false), and prevent layout for
        // performance gains.
        this._constraint.lock();
        this.mutate(nonBoundsOptions);
        this._constraint.unlock();
        // Update the layout (so that it is done once if we have resize:false)
        this._constraint.updateLayout();
        // After we have our localBounds complete, now we can mutate things that rely on it.
        this.mutate(boundsOptions);
        this.linkLayoutBounds();
    }
    /**
     * Sets the children of the GridBox and adjusts them to be positioned in certain cells. It takes a 2-dimensional array
     * of Node|null (where null is a placeholder that does nothing).
     *
     * For each cell, the first index into the array will be taken as the cell position in the provided orientation. The
     * second index into the array will be taken as the cell position in the OPPOSITE orientation.
     *
     * See GridBox.rows or GridBox.columns for usages and more documentation.
     */
    setLines(orientation, lineArrays) {
        const children = [];
        for (let i = 0; i < lineArrays.length; i++) {
            const lineArray = lineArrays[i];
            for (let j = 0; j < lineArray.length; j++) {
                const item = lineArray[j];
                if (item !== null) {
                    children.push(item);
                    item.mutateLayoutOptions({
                        [orientation.line]: i,
                        [orientation.opposite.line]: j
                    });
                }
            }
        }
        this.children = children;
    }
    /**
     * Returns the children of the GridBox in a 2-dimensional array of Node|null (where null is a placeholder that does
     * nothing).
     *
     * For each cell, the first index into the array will be taken as the cell position in the provided orientation. The
     * second index into the array will be taken as the cell position in the OPPOSITE orientation.
     *
     * See GridBox.rows or GridBox.columns for usages
     */
    getLines(orientation) {
        const lineArrays = [];
        for (const cell of this._cellMap.values()) {
            const i = cell.position.get(orientation);
            const j = cell.position.get(orientation.opposite);
            // Ensure we have enough lines
            while (lineArrays.length < i + 1) {
                lineArrays.push([]);
            }
            // null-pad lines
            while (lineArrays[i].length < j + 1) {
                lineArrays[i].push(null);
            }
            // Finally the actual node!
            lineArrays[i][j] = cell.node;
        }
        return lineArrays;
    }
    /**
     * Sets the children of the GridBox by specifying a two-dimensional array of Nodes (or null values as spacers).
     * The inner arrays will be the rows of the grid.
     * Mutates layoutOptions of the provided Nodes. See setLines() for more documentation.
     */
    set rows(lineArrays) {
        this.setLines(Orientation.VERTICAL, lineArrays);
    }
    /**
     * Returns a two-dimensional array of the child Nodes (with null as a spacer) where the inner arrays are the rows.
     */
    get rows() {
        return this.getLines(Orientation.VERTICAL);
    }
    /**
     * Sets the children of the GridBox by specifying a two-dimensional array of Nodes (or null values as spacers).
     * The inner arrays will be the columns of the grid.
     * * Mutates layoutOptions of the provided Nodes. See setLines() for more documentation.
     */
    set columns(lineArrays) {
        this.setLines(Orientation.HORIZONTAL, lineArrays);
    }
    /**
     * Returns a two-dimensional array of the child Nodes (with null as a spacer) where the inner arrays are the columns.
     */
    get columns() {
        return this.getLines(Orientation.HORIZONTAL);
    }
    /**
     * Returns the Node at a specific row/column intersection (or null if there are none)
     */
    getNodeAt(row, column) {
        const cell = this.constraint.getCell(row, column);
        return cell ? cell.node : null;
    }
    /**
     * Returns the row index of a child Node (or if it spans multiple rows, the first row)
     */
    getRowOfNode(node) {
        assert && assert(this.children.includes(node));
        return this.constraint.getCellFromNode(node).position.vertical;
    }
    /**
     * Returns the column index of a child Node (or if it spans multiple columns, the first row)
     */
    getColumnOfNode(node) {
        assert && assert(this.children.includes(node));
        return this.constraint.getCellFromNode(node).position.horizontal;
    }
    /**
     * Returns all the Nodes in a given row (by index)
     */
    getNodesInRow(index) {
        return this.constraint.getCells(Orientation.VERTICAL, index).map(cell => cell.node);
    }
    /**
     * Returns all the Nodes in a given column (by index)
     */
    getNodesInColumn(index) {
        return this.constraint.getCells(Orientation.HORIZONTAL, index).map(cell => cell.node);
    }
    /**
     * Adds an array of child Nodes (with null allowed as empty spacers) at the bottom of all existing rows.
     */
    addRow(row) {
        this.rows = [...this.rows, row];
        return this;
    }
    /**
     * Adds an array of child Nodes (with null allowed as empty spacers) at the right of all existing columns.
     */
    addColumn(column) {
        this.columns = [...this.columns, column];
        return this;
    }
    /**
     * Inserts a row of child Nodes at a given row index (see addRow for more information)
     */
    insertRow(index, row) {
        this.rows = [...this.rows.slice(0, index), row, ...this.rows.slice(index)];
        return this;
    }
    /**
     * Inserts a column of child Nodes at a given column index (see addColumn for more information)
     */
    insertColumn(index, column) {
        this.columns = [...this.columns.slice(0, index), column, ...this.columns.slice(index)];
        return this;
    }
    /**
     * Removes all child Nodes in a given row
     */
    removeRow(index) {
        this.rows = [...this.rows.slice(0, index), ...this.rows.slice(index + 1)];
        return this;
    }
    /**
     * Removes all child Nodes in a given column
     */
    removeColumn(index) {
        this.columns = [...this.columns.slice(0, index), ...this.columns.slice(index + 1)];
        return this;
    }
    set autoRows(value) {
        assert && assert(value === null || (typeof value === 'number' && isFinite(value) && value >= 1));
        if (this._autoRows !== value) {
            this._autoRows = value;
            this.updateAutoRows();
        }
    }
    get autoRows() {
        return this._autoRows;
    }
    set autoColumns(value) {
        assert && assert(value === null || (typeof value === 'number' && isFinite(value) && value >= 1));
        if (this._autoColumns !== value) {
            this._autoColumns = value;
            this.updateAutoColumns();
        }
    }
    get autoColumns() {
        return this._autoColumns;
    }
    // Used for autoRows/autoColumns
    updateAutoLines(orientation, value) {
        if (value !== null && this._autoLockCount === 0) {
            let updatedCount = 0;
            this.constraint.lock();
            this.children.filter(child => {
                return child.bounds.isValid() && (!this._constraint.excludeInvisible || child.visible);
            }).forEach((child, index) => {
                const primary = index % value;
                const secondary = Math.floor(index / value);
                const width = 1;
                const height = 1;
                // We guard to see if we actually have to update anything (so we can avoid triggering an auto-layout)
                if (!child.layoutOptions ||
                    child.layoutOptions[orientation.line] !== primary ||
                    child.layoutOptions[orientation.opposite.line] !== secondary ||
                    child.layoutOptions.horizontalSpan !== width ||
                    child.layoutOptions.verticalSpan !== height) {
                    updatedCount++;
                    child.mutateLayoutOptions({
                        [orientation.line]: index % value,
                        [orientation.opposite.line]: Math.floor(index / value),
                        horizontalSpan: 1,
                        verticalSpan: 1
                    });
                }
            });
            this.constraint.unlock();
            // Only trigger an automatic layout IF we actually adjusted something.
            if (updatedCount > 0) {
                this.constraint.updateLayoutAutomatically();
            }
        }
    }
    updateAutoRows() {
        this.updateAutoLines(Orientation.VERTICAL, this.autoRows);
    }
    updateAutoColumns() {
        this.updateAutoLines(Orientation.HORIZONTAL, this.autoColumns);
    }
    // Updates rows or columns, whichever is active at the moment (if any)
    updateAllAutoLines() {
        assert && assert(this._autoRows === null || this._autoColumns === null, 'autoRows and autoColumns should not both be set when updating children');
        this.updateAutoRows();
        this.updateAutoColumns();
    }
    setChildren(children) {
        const oldChildren = this.getChildren(); // defensive copy
        // Don't update autoRows/autoColumns settings while setting children, wait until after for performance
        this._autoLockCount++;
        super.setChildren(children);
        this._autoLockCount--;
        if (!_.isEqual(oldChildren, children)) {
            this.updateAllAutoLines();
        }
        return this;
    }
    /**
     * Called when a child is inserted.
     */
    onGridBoxChildInserted(node, index) {
        node.visibleProperty.lazyLink(this.onChildVisibilityToggled);
        const cell = new GridCell(this._constraint, node, this._constraint.createLayoutProxy(node));
        this._cellMap.set(node, cell);
        this._constraint.addCell(cell);
        this.updateAllAutoLines();
    }
    /**
     * Called when a child is removed.
     *
     * NOTE: This is NOT called on disposal. Any additional cleanup (to prevent memory leaks) should be included in the
     * dispose() function
     */
    onGridBoxChildRemoved(node) {
        const cell = this._cellMap.get(node);
        assert && assert(cell);
        this._cellMap.delete(node);
        this._constraint.removeCell(cell);
        cell.dispose();
        this.updateAllAutoLines();
        node.visibleProperty.unlink(this.onChildVisibilityToggled);
    }
    mutate(options) {
        // children can be used with one of autoRows/autoColumns, but otherwise these options are exclusive
        assertMutuallyExclusiveOptions(options, ['rows'], ['columns'], ['children', 'autoRows', 'autoColumns']);
        if (options) {
            assert && assert(typeof options.autoRows !== 'number' || typeof options.autoColumns !== 'number', 'autoRows and autoColumns should not be specified both as non-null at the same time');
        }
        return super.mutate(options);
    }
    get spacing() {
        return this._constraint.spacing;
    }
    set spacing(value) {
        this._constraint.spacing = value;
    }
    get xSpacing() {
        return this._constraint.xSpacing;
    }
    set xSpacing(value) {
        this._constraint.xSpacing = value;
    }
    get ySpacing() {
        return this._constraint.ySpacing;
    }
    set ySpacing(value) {
        this._constraint.ySpacing = value;
    }
    get xAlign() {
        return this._constraint.xAlign;
    }
    set xAlign(value) {
        this._constraint.xAlign = value;
    }
    get yAlign() {
        return this._constraint.yAlign;
    }
    set yAlign(value) {
        this._constraint.yAlign = value;
    }
    get grow() {
        return this._constraint.grow;
    }
    set grow(value) {
        this._constraint.grow = value;
    }
    get xGrow() {
        return this._constraint.xGrow;
    }
    set xGrow(value) {
        this._constraint.xGrow = value;
    }
    get yGrow() {
        return this._constraint.yGrow;
    }
    set yGrow(value) {
        this._constraint.yGrow = value;
    }
    get stretch() {
        return this._constraint.stretch;
    }
    set stretch(value) {
        this._constraint.stretch = value;
    }
    get xStretch() {
        return this._constraint.xStretch;
    }
    set xStretch(value) {
        this._constraint.xStretch = value;
    }
    get yStretch() {
        return this._constraint.yStretch;
    }
    set yStretch(value) {
        this._constraint.yStretch = value;
    }
    get margin() {
        return this._constraint.margin;
    }
    set margin(value) {
        this._constraint.margin = value;
    }
    get xMargin() {
        return this._constraint.xMargin;
    }
    set xMargin(value) {
        this._constraint.xMargin = value;
    }
    get yMargin() {
        return this._constraint.yMargin;
    }
    set yMargin(value) {
        this._constraint.yMargin = value;
    }
    get leftMargin() {
        return this._constraint.leftMargin;
    }
    set leftMargin(value) {
        this._constraint.leftMargin = value;
    }
    get rightMargin() {
        return this._constraint.rightMargin;
    }
    set rightMargin(value) {
        this._constraint.rightMargin = value;
    }
    get topMargin() {
        return this._constraint.topMargin;
    }
    set topMargin(value) {
        this._constraint.topMargin = value;
    }
    get bottomMargin() {
        return this._constraint.bottomMargin;
    }
    set bottomMargin(value) {
        this._constraint.bottomMargin = value;
    }
    get minContentWidth() {
        return this._constraint.minContentWidth;
    }
    set minContentWidth(value) {
        this._constraint.minContentWidth = value;
    }
    get minContentHeight() {
        return this._constraint.minContentHeight;
    }
    set minContentHeight(value) {
        this._constraint.minContentHeight = value;
    }
    get maxContentWidth() {
        return this._constraint.maxContentWidth;
    }
    set maxContentWidth(value) {
        this._constraint.maxContentWidth = value;
    }
    get maxContentHeight() {
        return this._constraint.maxContentHeight;
    }
    set maxContentHeight(value) {
        this._constraint.maxContentHeight = value;
    }
    setExcludeInvisibleChildrenFromBounds(excludeInvisibleChildrenFromBounds) {
        super.setExcludeInvisibleChildrenFromBounds(excludeInvisibleChildrenFromBounds);
        this.updateAllAutoLines();
    }
    dispose() {
        // Lock our layout forever
        this._constraint.lock();
        this.childInsertedEmitter.removeListener(this.onChildInserted);
        this.childRemovedEmitter.removeListener(this.onChildRemoved);
        // Dispose our cells here. We won't be getting the children-removed listeners fired (we removed them above)
        for (const cell of this._cellMap.values()) {
            cell.dispose();
            cell.node.visibleProperty.unlink(this.onChildVisibilityToggled);
        }
        super.dispose();
    }
    getHelperNode() {
        const marginsNode = MarginLayoutCell.createHelperNode(this.constraint.displayedCells, this.constraint.layoutBoundsProperty.value, cell => {
            let str = '';
            str += `row: ${cell.position.vertical}\n`;
            str += `column: ${cell.position.horizontal}\n`;
            if (cell.size.horizontal > 1) {
                str += `horizontalSpan: ${cell.size.horizontal}\n`;
            }
            if (cell.size.vertical > 1) {
                str += `verticalSpan: ${cell.size.vertical}\n`;
            }
            str += `xAlign: ${LayoutAlign.internalToAlign(Orientation.HORIZONTAL, cell.effectiveXAlign)}\n`;
            str += `yAlign: ${LayoutAlign.internalToAlign(Orientation.VERTICAL, cell.effectiveYAlign)}\n`;
            str += `xStretch: ${cell.effectiveXStretch}\n`;
            str += `yStretch: ${cell.effectiveYStretch}\n`;
            str += `xGrow: ${cell.effectiveXGrow}\n`;
            str += `yGrow: ${cell.effectiveYGrow}\n`;
            return str;
        });
        return marginsNode;
    }
}
/**
 * {Array.<string>} - String keys for all of the allowed options that will be set by node.mutate( options ), in the
 * order they will be evaluated in.
 *
 * NOTE: See Node's _mutatorKeys documentation for more information on how this operates, and potential special
 *       cases that may apply.
 */
GridBox.prototype._mutatorKeys = [...SIZABLE_OPTION_KEYS, ...GRIDBOX_OPTION_KEYS, ...Node.prototype._mutatorKeys];
scenery.register('GridBox', GridBox);
//# sourceMappingURL=GridBox.js.map