import '../../../../assert/js/assert.js';
import '../../../../globals.js';
import TinyEmitter from '../../../../axon/js/TinyEmitter.js';
import { extendsHeightSizable, extendsWidthSizable, LayoutProxy, scenery } from '../../imports.js';
export default class LayoutConstraint {
    /**
     * (scenery-internal)
     */
    constructor(ancestorNode) {
        // Prevents layout() from running while greater than zero. Generally will be unlocked and laid out.
        // See the documentation at the top of the file for more on reentrancy.
        this._layoutLockCount = 0;
        // Whether there was a layout attempt during the lock
        this._layoutAttemptDuringLock = false;
        // When we are disabled (say, a layout container has resize:false), we won't automatically do layout
        this._enabled = true;
        // Track Nodes we're listening to (for memory cleanup purposes)
        this._listenedNodes = new Set();
        // (scenery-internal) - emits when we've finished layout
        this.finishedLayoutEmitter = new TinyEmitter();
        this.ancestorNode = ancestorNode;
        this._updateLayoutListener = this.updateLayoutAutomatically.bind(this);
    }
    /**
     * Adds listeners to a Node, so that our layout updates will happen if this Node's Properties
     * (bounds/visibility/minimum size) change. Will be cleared on disposal of this type.
     * (scenery-internal)
     *
     * @param node
     * @param addLock - If true, we'll mark the node as having this layout constraint as responsible for its layout.
     * It will be an assertion failure if another layout container tries to lock the same node (so that we don't run into
     * infinite loops where multiple constraints try to move a node back-and-forth).
     * See Node's _activeParentLayoutConstraint for more information.
     */
    addNode(node, addLock = true) {
        assert && assert(!this._listenedNodes.has(node));
        assert && assert(!addLock || !node._activeParentLayoutConstraint, 'This node is already managed by a layout container - make sure to wrap it in a Node if DAG, removing it from an old layout container, etc.');
        if (addLock) {
            node._activeParentLayoutConstraint = this;
        }
        node.boundsProperty.lazyLink(this._updateLayoutListener);
        node.visibleProperty.lazyLink(this._updateLayoutListener);
        if (extendsWidthSizable(node)) {
            node.minimumWidthProperty.lazyLink(this._updateLayoutListener);
            node.isWidthResizableProperty.lazyLink(this._updateLayoutListener);
        }
        if (extendsHeightSizable(node)) {
            node.minimumHeightProperty.lazyLink(this._updateLayoutListener);
            node.isHeightResizableProperty.lazyLink(this._updateLayoutListener);
        }
        this._listenedNodes.add(node);
    }
    /**
     * (scenery-internal)
     */
    removeNode(node) {
        assert && assert(this._listenedNodes.has(node));
        // Optional, since we might not have added the "lock" in addNode
        if (node._activeParentLayoutConstraint === this) {
            node._activeParentLayoutConstraint = null;
        }
        node.boundsProperty.unlink(this._updateLayoutListener);
        node.visibleProperty.unlink(this._updateLayoutListener);
        if (extendsWidthSizable(node)) {
            node.minimumWidthProperty.unlink(this._updateLayoutListener);
            node.isWidthResizableProperty.unlink(this._updateLayoutListener);
        }
        if (extendsHeightSizable(node)) {
            node.minimumHeightProperty.unlink(this._updateLayoutListener);
            node.isHeightResizableProperty.unlink(this._updateLayoutListener);
        }
        this._listenedNodes.delete(node);
    }
    /**
     * NOTE: DO NOT call from places other than super.layout() in overridden layout() OR from the existing call in
     *       updateLayout(). Doing so would break the lock mechanism.
     * NOTE: Cannot be marked as abstract due to how mixins work
     */
    layout() {
        // See subclass for implementation
    }
    /**
     * (scenery-internal)
     */
    get isLocked() {
        return this._layoutLockCount > 0;
    }
    /**
     * Locks the layout, so that automatic layout will NOT be triggered synchronously until unlock() is called and
     * the lock count returns to 0. This is set up so that if we trigger multiple reentrancy, we will only attempt to
     * re-layout once ALL of the layouts are finished.
     * (scenery-internal)
     */
    lock() {
        this._layoutLockCount++;
    }
    /**
     * Unlocks the layout. Generally (but not always), updateLayout() or updateLayoutAutomatically() should be called
     * after this, as locks are generally used for this purpose.
     * (scenery-internal)
     */
    unlock() {
        this._layoutLockCount--;
    }
    /**
     * Here for manual validation (say, in the devtools) - While some layouts are going on, this may not be correct, so it
     * could not be added to post-layout validation.
     * (scenery-internal)
     */
    validateLocalPreferredWidth(layoutContainer) {
        if (assert && layoutContainer.localBounds.isFinite() && !this._layoutAttemptDuringLock) {
            layoutContainer.validateLocalPreferredWidth();
        }
    }
    /**
     * Here for manual validation (say, in the devtools) - While some layouts are going on, this may not be correct, so it
     * could not be added to post-layout validation.
     * (scenery-internal)
     */
    validateLocalPreferredHeight(layoutContainer) {
        if (assert && layoutContainer.localBounds.isFinite() && !this._layoutAttemptDuringLock) {
            layoutContainer.validateLocalPreferredHeight();
        }
    }
    /**
     * Here for manual validation (say, in the devtools) - While some layouts are going on, this may not be correct, so it
     * could not be added to post-layout validation.
     * (scenery-internal)
     */
    validateLocalPreferredSize(layoutContainer) {
        if (assert && layoutContainer.localBounds.isFinite() && !this._layoutAttemptDuringLock) {
            layoutContainer.validateLocalPreferredSize();
        }
    }
    /**
     * Updates the layout of this constraint. Called automatically during initialization, when children change (if
     * resize is true), or when client wants to call this public method for any reason.
     */
    updateLayout() {
        let count = 0;
        // If we're locked AND someone tries to do layout, record this so we can attempt layout once we are not locked
        // anymore. We have some infinite-loop detection here for common development errors.
        if (this.isLocked) {
            assert && count++;
            assert && assert(++count < 500, 'Likely infinite loop detected, are we triggering layout within the layout?');
            this._layoutAttemptDuringLock = true;
        }
        else {
            this.lock();
            // Run layout until we didn't get a layout attempt during our last attempt. This component's layout should now
            // be correct and stable.
            do {
                this._layoutAttemptDuringLock = false;
                this.layout();
            } 
            // If we got any layout attempts during the lock, we'll want to rerun the layout
            while (this._layoutAttemptDuringLock);
            this.unlock();
        }
    }
    /**
     * Called when we attempt to automatically layout components. (scenery-internal)
     */
    updateLayoutAutomatically() {
        if (this._enabled) {
            this.updateLayout();
        }
    }
    /**
     * Creates a LayoutProxy for a unique trail from our ancestorNode to this Node (or null if that's not possible)
     * (scenery-internal)
     */
    createLayoutProxy(node) {
        const trails = node.getTrails(n => n === this.ancestorNode);
        if (trails.length === 1) {
            return LayoutProxy.pool.create(trails[0].removeAncestor());
        }
        else {
            return null;
        }
    }
    get enabled() {
        return this._enabled;
    }
    set enabled(value) {
        if (this._enabled !== value) {
            this._enabled = value;
            this.updateLayoutAutomatically();
        }
    }
    /**
     * Releases references
     */
    dispose() {
        // Clean up listeners to any listened nodes
        const listenedNodes = [...this._listenedNodes.keys()];
        for (let i = 0; i < listenedNodes.length; i++) {
            this.removeNode(listenedNodes[i]);
        }
        this.finishedLayoutEmitter.dispose();
    }
}
scenery.register('LayoutConstraint', LayoutConstraint);
//# sourceMappingURL=LayoutConstraint.js.map