import '../../../assert/js/assert.js';
import '../../../globals.js';
// Copyright 2024, University of Colorado Boulder
/**
 * A Property that will contain a list of Trails where the root of the trail is a root Node of a Display, and the leaf
 * node is the provided Node.
 *
 * // REVIEW: This is a very complicated component and deserves a bit more doc. Some ideas about what to explain:
 * // REVIEW:   1. That this is synchronously updated and doesn't listen to instances.
 * // REVIEW:   2.
 * // REVIEW:   2.
 * // REVIEW:   2.
 *
 * // REVIEW: can you describe this a bit more. Do you mean any Node in a trail? What about if the provided Node is disposed?
 * NOTE: If a Node is disposed, it will be removed from the trails.
 *
 * @author Jonathan Olson <jonathan.olson@colorado.edu>
 */
import TinyProperty from '../../../axon/js/TinyProperty.js';
import optionize from '../../../phet-core/js/optionize.js';
import { Display, scenery, Trail } from '../imports.js';
export default class DisplayedTrailsProperty extends TinyProperty {
    /**
     * We will contain Trails whose leaf node (lastNode) is this provided Node.
     */
    constructor(node, providedOptions) {
        const options = optionize()({
            // Listen to all displays
            display: null,
            // Default to visual trails (just children), with only pruning by normal visibility
            followPDOMOrder: false,
            requireVisible: true,
            requirePDOMVisible: false,
            requireEnabled: false,
            requireInputEnabled: false
        }, providedOptions);
        super([]);
        // REVIEW: Please add doc why we only need to listen to a Node once, even if it is in multiple trails?
        this.listenedNodeSet = new Set();
        // Save options for later updates
        this.node = node;
        this.display = options.display;
        this.followPDOMOrder = options.followPDOMOrder;
        this.requireVisible = options.requireVisible;
        this.requirePDOMVisible = options.requirePDOMVisible;
        this.requireEnabled = options.requireEnabled;
        this.requireInputEnabled = options.requireInputEnabled;
        this._trailUpdateListener = this.update.bind(this);
        this.update();
    }
    update() {
        // Factored out because we're using a "function" below for recursion (NOT an arrow function)
        const display = this.display;
        const followPDOMOrder = this.followPDOMOrder;
        const requireVisible = this.requireVisible;
        const requirePDOMVisible = this.requirePDOMVisible;
        const requireEnabled = this.requireEnabled;
        const requireInputEnabled = this.requireInputEnabled;
        // Trails accumulated in our recursion that will be our Property's value
        const trails = [];
        // Nodes that were touched in the scan (we should listen to changes to ANY of these to see if there is a connection
        // or disconnection). This could potentially cause our Property to change
        const nodeSet = new Set();
        // Modified in-place during the search
        const trail = new Trail(this.node);
        // We will recursively add things to the "front" of the trail (ancestors)
        (function recurse() {
            const root = trail.rootNode();
            // If a Node is disposed, we won't add listeners to it, so we abort slightly earlier.
            if (root.isDisposed) {
                return;
            }
            nodeSet.add(root);
            // REVIEW: Please say why we need listeners on this Node. Also please confirm (via doc) that adding
            // If we fail other conditions, we won't add a trail OR recurse, but we will STILL have listeners added to the Node.
            if ((requireVisible && !root.visible) ||
                (requirePDOMVisible && !root.pdomVisible) ||
                (requireEnabled && !root.enabled) ||
                (requireInputEnabled && !root.inputEnabled)) {
                return;
            }
            const displays = root.getRootedDisplays();
            // REVIEW: initialize to false?
            let displayMatches;
            if (display === null) {
                displayMatches = displays.length > 0;
            }
            else if (display instanceof Display) {
                displayMatches = displays.includes(display);
            }
            else {
                displayMatches = displays.some(display);
            }
            if (displayMatches) {
                // Create a permanent copy that won't be mutated
                trails.push(trail.copy());
            }
            // REVIEW: I'm officially confused about this feature. What is the value of "either or", why not be able to
            // support both visual and PDOM in the same Property? If this is indeed best, please be sure to explain where
            // the option is defined.
            const parents = followPDOMOrder && root.pdomParent ? [root.pdomParent] : root.parents;
            parents.forEach(parent => {
                trail.addAncestor(parent);
                recurse();
                trail.removeAncestor();
            });
        })();
        // REVIEW: Webstorm flagged the next 29 lines as duplicated with TrailsBetweenProperty. Let's factor that our or fix that somehow.
        // Add in new needed listeners
        nodeSet.forEach(node => {
            if (!this.listenedNodeSet.has(node)) {
                this.addNodeListener(node);
            }
        });
        // Remove listeners not needed anymore
        this.listenedNodeSet.forEach(node => {
            if (!nodeSet.has(node)) {
                this.removeNodeListener(node);
            }
        });
        // Guard in a way that deepEquality on the Property wouldn't (because of the Array wrapper)
        // NOTE: Duplicated with TrailsBetweenProperty, likely can be factored out.
        // REVIEW: ^^^^ +1, yes please factor out.
        const currentTrails = this.value;
        let trailsEqual = currentTrails.length === trails.length;
        if (trailsEqual) {
            for (let i = 0; i < trails.length; i++) {
                if (!currentTrails[i].equals(trails[i])) {
                    trailsEqual = false;
                    break;
                }
            }
        }
        // REVIEW: Can this be improved upon by utilizing a custom valueComparisonStrategy? I don't see that being much
        // less performant given that you are doing all the above work on each call to update().
        if (!trailsEqual) {
            this.value = trails;
        }
    }
    // REVIEW: Rename to either `addNodeListeners`, or something more general like `listenToNode()`.
    addNodeListener(node) {
        this.listenedNodeSet.add(node);
        // Unconditional listeners, which affect all nodes.
        node.parentAddedEmitter.addListener(this._trailUpdateListener);
        node.parentRemovedEmitter.addListener(this._trailUpdateListener);
        node.rootedDisplayChangedEmitter.addListener(this._trailUpdateListener);
        node.disposeEmitter.addListener(this._trailUpdateListener);
        if (this.followPDOMOrder) {
            node.pdomParentChangedEmitter.addListener(this._trailUpdateListener);
        }
        if (this.requireVisible) {
            node.visibleProperty.lazyLink(this._trailUpdateListener);
        }
        if (this.requirePDOMVisible) {
            node.pdomVisibleProperty.lazyLink(this._trailUpdateListener);
        }
        if (this.requireEnabled) {
            node.enabledProperty.lazyLink(this._trailUpdateListener);
        }
        if (this.requireInputEnabled) {
            node.inputEnabledProperty.lazyLink(this._trailUpdateListener);
        }
    }
    removeNodeListener(node) {
        this.listenedNodeSet.delete(node);
        node.parentAddedEmitter.removeListener(this._trailUpdateListener);
        node.parentRemovedEmitter.removeListener(this._trailUpdateListener);
        node.rootedDisplayChangedEmitter.removeListener(this._trailUpdateListener);
        node.disposeEmitter.removeListener(this._trailUpdateListener);
        if (this.followPDOMOrder) {
            node.pdomParentChangedEmitter.removeListener(this._trailUpdateListener);
        }
        if (this.requireVisible) {
            node.visibleProperty.unlink(this._trailUpdateListener);
        }
        if (this.requirePDOMVisible) {
            node.pdomVisibleProperty.unlink(this._trailUpdateListener);
        }
        if (this.requireEnabled) {
            node.enabledProperty.unlink(this._trailUpdateListener);
        }
        if (this.requireInputEnabled) {
            node.inputEnabledProperty.unlink(this._trailUpdateListener);
        }
    }
    // REVIEW: I always forget why you don't need to also clear your reference to the provided Node. Do you?
    // REVIEW: Also maybe assert here that your provided node is in this listened to Node set?
    dispose() {
        this.listenedNodeSet.forEach(node => this.removeNodeListener(node));
        super.dispose();
    }
}
scenery.register('DisplayedTrailsProperty', DisplayedTrailsProperty);
//# sourceMappingURL=DisplayedTrailsProperty.js.map