import '../../../globals.js';
// Copyright 2017-2024, University of Colorado Boulder
/**
 * A MultiListener that is designed to pan and zoom a target Node, where you can provide limiting and
 * describing bounds so that the targetNode is limited to a region.
 *
 * @author Jonathan Olson <jonathan.olson@colorado.edu>
 * @author Jesse Greenberg (PhET Interactive Simulations)
 */
import Property from '../../../axon/js/Property.js';
import Bounds2 from '../../../dot/js/Bounds2.js';
import Matrix3 from '../../../dot/js/Matrix3.js';
import optionize from '../../../phet-core/js/optionize.js';
import ModelViewTransform2 from '../../../phetcommon/js/view/ModelViewTransform2.js';
import isSettingPhetioStateProperty from '../../../tandem/js/isSettingPhetioStateProperty.js';
import { MultiListener, scenery } from '../imports.js';
// constants
// Reusable Matrix3 instance to avoid creating lots of them
const SCRATCH_MATRIX = new Matrix3();
class PanZoomListener extends MultiListener {
    /**
     * @param targetNode - The Node that should be transformed by this PanZoomListener.
     * @param [providedOptions].
     */
    constructor(targetNode, providedOptions) {
        const options = optionize()({
            panBounds: Bounds2.NOTHING,
            targetBounds: null,
            targetScale: 1,
            // by default, the PanZoomListener does now allow rotation
            allowRotation: false
        }, providedOptions);
        super(targetNode, options);
        this._panBounds = options.panBounds;
        this._targetBounds = options.targetBounds || targetNode.globalBounds.copy();
        this._targetScale = options.targetScale;
        this.sourceFramePanBoundsProperty = new Property(this._panBounds, {
            tandem: options.tandem?.createTandem('sourceFramePanBoundsProperty'),
            phetioReadOnly: true,
            phetioValueType: Bounds2.Bounds2IO
        });
        this.sourceFramePanBoundsProperty.lazyLink(() => {
            if (isSettingPhetioStateProperty.value) {
                // The matrixProperty has transformations relative to the global view coordinates of the source simulation,
                // so it will not be correct if source and destination frames are different sizes. This will map transforamtions
                // if destination frame has different size.
                const sourceDestinationTransform = ModelViewTransform2.createRectangleMapping(this.sourceFramePanBoundsProperty.get(), this._panBounds);
                const newTranslation = this._targetNode.matrix.translation.componentMultiply(sourceDestinationTransform.matrix.getScaleVector());
                const scale = this.matrixProperty.get().getScaleVector();
                this.matrixProperty.set(Matrix3.translationFromVector(newTranslation).timesMatrix(Matrix3.scaling(scale.x, scale.y)));
            }
        }, {
            // so that the listener will be called only after the matrixProperty is up to date in the downstream sim
            phetioDependencies: [this.matrixProperty]
        });
    }
    /**
     * If the targetNode is larger than the panBounds specified, keep the panBounds completely filled with
     * targetNode content.
     */
    correctReposition() {
        // Save values of the current matrix, so that we only do certain work when the matrix actually changes
        SCRATCH_MATRIX.set(this._targetNode.matrix);
        // the targetBounds transformed by the targetNode's transform, to determine if targetBounds are out of panBounds
        const transformedBounds = this._targetBounds.transformed(this._targetNode.getMatrix());
        // Don't let panning go through if the node is fully contained by the panBounds
        if (transformedBounds.left > this._panBounds.left) {
            this._targetNode.left = this._panBounds.left - (transformedBounds.left - this._targetNode.left);
        }
        if (transformedBounds.top > this._panBounds.top) {
            this._targetNode.top = this._panBounds.top - (transformedBounds.top - this._targetNode.top);
        }
        if (transformedBounds.right < this._panBounds.right) {
            this._targetNode.right = this._panBounds.right + (this._targetNode.right - transformedBounds.right);
        }
        if (transformedBounds.bottom < this._panBounds.bottom) {
            this._targetNode.bottom = this._panBounds.bottom + (this._targetNode.bottom - transformedBounds.bottom);
        }
        // Update Property with matrix once position has been corrected to notify listeners and set PhET-iO state, but
        // only notify when there has been an actual change.
        if (!SCRATCH_MATRIX.equals(this._targetNode.matrix)) {
            this.matrixProperty.set(this._targetNode.matrix.copy());
        }
    }
    /**
     * If the transformed targetBounds are equal to the panBounds, there is no space for us to pan so do not change
     * the pointer cursor.
     */
    addPress(press) {
        super.addPress(press);
        // don't show the pressCursor if our bounds are limited by pan bounds, and we cannot pan anywhere
        const transformedBounds = this._targetBounds.transformed(this._targetNode.getMatrix());
        const boundsLimited = transformedBounds.equalsEpsilon(this._panBounds, 1E-8);
        press.pointer.cursor = boundsLimited ? null : this._pressCursor;
    }
    /**
     * Reposition but keep content within this._panBounds.
     */
    reposition() {
        super.reposition();
        this.correctReposition();
    }
    /**
     * Reset the transform on the targetNode and follow up by making sure that the content is still within panBounds.
     */
    resetTransform() {
        super.resetTransform();
        this.correctReposition();
    }
    /**
     * Set the containing panBounds and then make sure that the targetBounds fully fill the new panBounds.
     */
    setPanBounds(panBounds) {
        this._panBounds = panBounds;
        this.sourceFramePanBoundsProperty.set(this._panBounds);
        this.correctReposition();
    }
    /**
     * Set the targetBounds which should totally fill the panBounds at all times. Useful if the targetNode has bounds
     * which don't accurately describe the node. For instance, if an overlay plane is on top of the node and extends
     * beyond the dimensions of the visible node.
     *
     * targetBounds - in the global coordinate frame
     */
    setTargetBounds(targetBounds) {
        this._targetBounds = targetBounds;
        this.correctReposition();
    }
    /**
     * Set the representative scale of the target Node. If the targetBounds are different from the targetNode.bounds
     * it may be useful to correct changes to panning and zooming by a scale that is different from the
     * actual scale applied to the targetNode during panning.
     */
    setTargetScale(scale) {
        this._targetScale = scale;
    }
    /**
     * Get the targetBounds, in the global coordinate frame.
     */
    getTargetBounds() {
        return this._targetBounds;
    }
}
scenery.register('PanZoomListener', PanZoomListener);
export default PanZoomListener;
//# sourceMappingURL=PanZoomListener.js.map