import '../../../../assert/js/assert.js';
import '../../../../globals.js';
// Copyright 2021-2024, University of Colorado Boulder
/**
 * A LayoutCell that has margins, and can be positioned and sized relative to those. Used for Flow/Grid layouts
 *
 * @author Jonathan Olson <jonathan.olson@colorado.edu>
 */
import Bounds2 from '../../../../dot/js/Bounds2.js';
import Matrix3 from '../../../../dot/js/Matrix3.js';
import Utils from '../../../../dot/js/Utils.js';
import { Shape } from '../../../../kite/js/imports.js';
import Orientation from '../../../../phet-core/js/Orientation.js';
import OrientationPair from '../../../../phet-core/js/OrientationPair.js';
import Tandem from '../../../../tandem/js/Tandem.js';
import { Font, LayoutAlign, LayoutCell, Node, NodePattern, Path, PressListener, Rectangle, RichText, scenery, Text } from '../../imports.js';
// NOTE: This would be an abstract class, but that is incompatible with how mixin constraints work in TypeScript
export default class MarginLayoutCell extends LayoutCell {
    /**
     * NOTE: Consider this scenery-internal AND protected. It's effectively a protected constructor for an abstract type,
     * but cannot be due to how mixins constrain things (TypeScript doesn't work with private/protected things like this)
     *
     * (scenery-internal)
     */
    constructor(constraint, node, proxy) {
        super(constraint, node, proxy);
        this.preferredSizeSet = new OrientationPair(false, false);
        // (scenery-internal) Set to be the bounds available for the cell
        this.lastAvailableBounds = Bounds2.NOTHING.copy();
        // (scenery-internal) Set to be the bounds used by the cell
        this.lastUsedBounds = Bounds2.NOTHING.copy();
        this._marginConstraint = constraint;
    }
    /**
     * Positions and sizes the cell (used for grid and flow layouts)
     * (scenery-internal)
     *
     * Returns the cell's bounds
     */
    reposition(orientation, lineSize, linePosition, stretch, originOffset, align) {
        // Mimicking https://www.w3.org/TR/css-flexbox-1/#align-items-property for baseline (for our origin)
        // Origin will sync all origin-based items (so their origin matches), and then position ALL of that as if it was
        // align:left or align:top (depending on the orientation).
        const preferredSize = (stretch && this.isSizable(orientation)) ? lineSize : this.getMinimumSize(orientation);
        if (assert) {
            const maxSize = orientation === Orientation.HORIZONTAL ? this.node.maxWidth : this.node.maxHeight;
            assert(!this.isSizable(orientation) || maxSize === null || Math.abs(maxSize - preferredSize) > -1e-9, `Tried to set a preferred size ${preferredSize} larger than the specified max${orientation === Orientation.HORIZONTAL ? 'Width' : 'Height'} of ${maxSize}. ` +
                'Ideally, try to avoid putting a maxWidth/maxHeight on a width/height-sizable Node (one that will resize to fit its preferred size) inside a layout container, ' +
                'particularly one that will try to expand the Node past its maximum size.');
        }
        this.attemptPreferredSize(orientation, preferredSize);
        if (align === LayoutAlign.ORIGIN) {
            this.positionOrigin(orientation, linePosition + originOffset);
        }
        else {
            this.positionStart(orientation, linePosition + (lineSize - this.getCellBounds()[orientation.size]) * align.padRatio);
        }
        const cellBounds = this.getCellBounds();
        assert && assert(cellBounds.isFinite());
        this.lastAvailableBounds[orientation.minCoordinate] = linePosition;
        this.lastAvailableBounds[orientation.maxCoordinate] = linePosition + lineSize;
        this.lastUsedBounds.set(cellBounds);
        return cellBounds;
    }
    /**
     * Returns the used value, with this cell's value taking precedence over the constraint's default
     * (scenery-internal)
     */
    get effectiveLeftMargin() {
        return this._leftMargin !== null ? this._leftMargin : this._marginConstraint._leftMargin;
    }
    /**
     * Returns the used value, with this cell's value taking precedence over the constraint's default
     * (scenery-internal)
     */
    get effectiveRightMargin() {
        return this._rightMargin !== null ? this._rightMargin : this._marginConstraint._rightMargin;
    }
    /**
     * Returns the used value, with this cell's value taking precedence over the constraint's default
     * (scenery-internal)
     */
    get effectiveTopMargin() {
        return this._topMargin !== null ? this._topMargin : this._marginConstraint._topMargin;
    }
    /**
     * Returns the used value, with this cell's value taking precedence over the constraint's default
     * (scenery-internal)
     */
    get effectiveBottomMargin() {
        return this._bottomMargin !== null ? this._bottomMargin : this._marginConstraint._bottomMargin;
    }
    /**
     * (scenery-internal)
     */
    getEffectiveMinMargin(orientation) {
        return orientation === Orientation.HORIZONTAL ? this.effectiveLeftMargin : this.effectiveTopMargin;
    }
    /**
     * (scenery-internal)
     */
    getEffectiveMaxMargin(orientation) {
        return orientation === Orientation.HORIZONTAL ? this.effectiveRightMargin : this.effectiveBottomMargin;
    }
    /**
     * Returns the used value, with this cell's value taking precedence over the constraint's default
     * (scenery-internal)
     */
    get effectiveMinContentWidth() {
        return this._minContentWidth !== null ? this._minContentWidth : this._marginConstraint._minContentWidth;
    }
    /**
     * Returns the used value, with this cell's value taking precedence over the constraint's default
     * (scenery-internal)
     */
    get effectiveMinContentHeight() {
        return this._minContentHeight !== null ? this._minContentHeight : this._marginConstraint._minContentHeight;
    }
    /**
     * (scenery-internal)
     */
    getEffectiveMinContent(orientation) {
        return orientation === Orientation.HORIZONTAL ? this.effectiveMinContentWidth : this.effectiveMinContentHeight;
    }
    /**
     * Returns the used value, with this cell's value taking precedence over the constraint's default
     * (scenery-internal)
     */
    get effectiveMaxContentWidth() {
        return this._maxContentWidth !== null ? this._maxContentWidth : this._marginConstraint._maxContentWidth;
    }
    /**
     * Returns the used value, with this cell's value taking precedence over the constraint's default
     * (scenery-internal)
     */
    get effectiveMaxContentHeight() {
        return this._maxContentHeight !== null ? this._maxContentHeight : this._marginConstraint._maxContentHeight;
    }
    /**
     * (scenery-internal)
     */
    getEffectiveMaxContent(orientation) {
        return orientation === Orientation.HORIZONTAL ? this.effectiveMaxContentWidth : this.effectiveMaxContentHeight;
    }
    /**
     * Returns the effective minimum size this cell can take (including the margins)
     * (scenery-internal)
     */
    getMinimumSize(orientation) {
        return this.getEffectiveMinMargin(orientation) +
            Math.max(this.proxy.getMinimum(orientation), this.getEffectiveMinContent(orientation) || 0) +
            this.getEffectiveMaxMargin(orientation);
    }
    /**
     * Returns the effective maximum size this cell can take (including the margins)
     * (scenery-internal)
     */
    getMaximumSize(orientation) {
        return this.getEffectiveMinMargin(orientation) +
            (this.getEffectiveMaxContent(orientation) || Number.POSITIVE_INFINITY) +
            this.getEffectiveMaxMargin(orientation);
    }
    /**
     * Sets a preferred size on the content, obeying many constraints.
     * (scenery-internal)
     */
    attemptPreferredSize(orientation, value) {
        if (this.proxy[orientation.sizable]) {
            const minimumSize = this.getMinimumSize(orientation);
            const maximumSize = this.getMaximumSize(orientation);
            assert && assert(isFinite(minimumSize));
            assert && assert(maximumSize >= minimumSize);
            value = Utils.clamp(value, minimumSize, maximumSize);
            let preferredSize = value - this.getEffectiveMinMargin(orientation) - this.getEffectiveMaxMargin(orientation);
            const maxSize = this.proxy.getMax(orientation);
            if (maxSize !== null) {
                preferredSize = Math.min(maxSize, preferredSize);
            }
            this._marginConstraint.setProxyPreferredSize(orientation, this.proxy, preferredSize);
            // Record that we set
            this.preferredSizeSet.set(orientation, true);
        }
    }
    /**
     * Unsets the preferred size (if WE set it)
     * (scenery-internal)
     */
    unsetPreferredSize(orientation) {
        if (this.proxy[orientation.sizable]) {
            this._marginConstraint.setProxyPreferredSize(orientation, this.proxy, null);
        }
    }
    /**
     * Sets the left/top position of the (content+margin) for the cell in the constraint's ancestor coordinate frame.
     * (scenery-internal)
     */
    positionStart(orientation, value) {
        const start = this.getEffectiveMinMargin(orientation) + value;
        this._marginConstraint.setProxyMinSide(orientation, this.proxy, start);
    }
    /**
     * Sets the x/y value of the content for the cell in the constraint's ancestor coordinate frame.
     * (scenery-internal)
     */
    positionOrigin(orientation, value) {
        this._marginConstraint.setProxyOrigin(orientation, this.proxy, value);
    }
    /**
     * Returns the bounding box of the cell if it was repositioned to have its origin shifted to the origin of the
     * ancestor node's local coordinate frame.
     * (scenery-internal)
     */
    getOriginBounds() {
        return this.getCellBounds().shiftedXY(-this.proxy.x, -this.proxy.y);
    }
    /**
     * The current bounds of the cell (with margins included)
     * (scenery-internal)
     */
    getCellBounds() {
        return this.proxy.bounds.withOffsets(this.effectiveLeftMargin, this.effectiveTopMargin, this.effectiveRightMargin, this.effectiveBottomMargin);
    }
    dispose() {
        // Unset the specified preferred sizes that were set by our layout (when we're removed)
        Orientation.enumeration.values.forEach(orientation => {
            if (this.preferredSizeSet.get(orientation)) {
                this.unsetPreferredSize(orientation);
            }
        });
        super.dispose();
    }
    static createHelperNode(cells, layoutBounds, cellToText) {
        const container = new Node();
        const lineWidth = 0.4;
        const availableCellsShape = Shape.union(cells.map(cell => Shape.bounds(cell.lastAvailableBounds)));
        const usedCellsShape = Shape.union(cells.map(cell => Shape.bounds(cell.lastUsedBounds)));
        const usedContentShape = Shape.union(cells.map(cell => Shape.bounds(cell.proxy.bounds)));
        const spacingShape = Shape.bounds(layoutBounds).shapeDifference(availableCellsShape);
        const emptyShape = availableCellsShape.shapeDifference(usedCellsShape);
        const marginShape = usedCellsShape.shapeDifference(usedContentShape);
        const createLabeledTexture = (label, foreground, background) => {
            const text = new Text(label, {
                font: new Font({ size: 6, family: 'monospace' }),
                fill: foreground
            });
            const rectangle = Rectangle.bounds(text.bounds, {
                fill: background,
                children: [text]
            });
            return new NodePattern(rectangle, 4, Math.floor(rectangle.left), Math.ceil(rectangle.top + 1), Math.floor(rectangle.width), Math.floor(rectangle.height - 2), Matrix3.rotation2(-Math.PI / 4));
        };
        container.addChild(new Path(spacingShape, {
            fill: createLabeledTexture('spacing', '#000', '#fff'),
            opacity: 0.6
        }));
        container.addChild(new Path(emptyShape, {
            fill: createLabeledTexture('empty', '#aaa', '#000'),
            opacity: 0.6
        }));
        container.addChild(new Path(marginShape, {
            fill: createLabeledTexture('margin', '#600', '#f00'),
            opacity: 0.6
        }));
        container.addChild(Rectangle.bounds(layoutBounds, {
            stroke: 'white',
            lineDash: [2, 2],
            lineDashOffset: 2,
            lineWidth: lineWidth
        }));
        container.addChild(Rectangle.bounds(layoutBounds, {
            stroke: 'black',
            lineDash: [2, 2],
            lineWidth: lineWidth
        }));
        cells.forEach(cell => {
            container.addChild(Rectangle.bounds(cell.getCellBounds(), {
                stroke: 'rgba(0,255,0,1)',
                lineWidth: lineWidth
            }));
        });
        cells.forEach(cell => {
            container.addChild(Rectangle.bounds(cell.proxy.bounds, {
                stroke: 'rgba(255,0,0,1)',
                lineWidth: lineWidth
            }));
        });
        cells.forEach(cell => {
            const bounds = cell.getCellBounds();
            const hoverListener = new PressListener({
                tandem: Tandem.OPT_OUT
            });
            container.addChild(Rectangle.bounds(bounds, {
                inputListeners: [hoverListener]
            }));
            let str = cellToText(cell);
            if (cell.effectiveLeftMargin) {
                str += `leftMargin: ${cell.effectiveLeftMargin}\n`;
            }
            if (cell.effectiveRightMargin) {
                str += `rightMargin: ${cell.effectiveRightMargin}\n`;
            }
            if (cell.effectiveTopMargin) {
                str += `topMargin: ${cell.effectiveTopMargin}\n`;
            }
            if (cell.effectiveBottomMargin) {
                str += `bottomMargin: ${cell.effectiveBottomMargin}\n`;
            }
            if (cell.effectiveMinContentWidth) {
                str += `minContentWidth: ${cell.effectiveMinContentWidth}\n`;
            }
            if (cell.effectiveMinContentHeight) {
                str += `minContentHeight: ${cell.effectiveMinContentHeight}\n`;
            }
            if (cell.effectiveMaxContentWidth) {
                str += `maxContentWidth: ${cell.effectiveMaxContentWidth}\n`;
            }
            if (cell.effectiveMaxContentHeight) {
                str += `maxContentHeight: ${cell.effectiveMaxContentHeight}\n`;
            }
            str += `layoutOptions: ${JSON.stringify(cell.node.layoutOptions, null, 2).replace(/ /g, '&nbsp;')}\n`;
            const hoverText = new RichText(str.trim().replace(/\n/g, '<br>'), {
                font: new Font({ size: 12 })
            });
            const hoverNode = Rectangle.bounds(hoverText.bounds.dilated(3), {
                fill: 'rgba(255,255,255,0.8)',
                children: [hoverText],
                leftTop: bounds.leftTop
            });
            container.addChild(hoverNode);
            hoverListener.isOverProperty.link(isOver => {
                hoverNode.visible = isOver;
            });
        });
        return container;
    }
}
scenery.register('MarginLayoutCell', MarginLayoutCell);
//# sourceMappingURL=MarginLayoutCell.js.map