import _ from 'lodash';
import '../../../assert/js/assert.js';
import '../../../tandem/js/PhetioIDUtils.js';
import '../../../globals.js';
// Copyright 2017-2024, University of Colorado Boulder
/**
 * PressListener subtype customized for handling most drag-related listener needs.
 *
 * DragListener uses some specific terminology that is helpful to understand:
 *
 * - Drag target: The node whose trail is used for coordinate transforms. When a targetNode is specified, it will be the
 *                drag target. Otherwise, whatever was the currentTarget during event bubbling for the event that
 *                triggered press will be used (almost always the node that the listener is added to).
 * - Global coordinate frame: Coordinate frame of the Display (specifically its rootNode's local coordinate frame),
 *                            that in some applications will be screen coordinates.
 * - Parent coordinate frame: The parent coordinate frame of our drag target. Basically, it's the coordinate frame
 *                            you'd need to use to set dragTarget.translation = <parent coordinate frame point> for the
 *                            drag target to follow the pointer.
 * - Local coordinate frame: The local coordinate frame of our drag target, where (0,0) would be at the drag target's
 *                           origin.
 * - Model coordinate frame: Optionally defined by a model-view transform (treating the parent coordinate frame as the
 *                           view). When a transform is provided, it's the coordinate frame needed for setting
 *                           dragModelElement.position = <model coordinate frame point>. If a transform is not provided
 *                           (or overridden), it will be the same as the parent coordinate frame.
 *
 * The typical coordinate handling of DragListener is to:
 * 1. When a drag is started (with press), record the pointer's position in the local coordinate frame. This is visually
 *    where the pointer is over the drag target, and typically most drags will want to move the dragged element so that
 *    the pointer continues to be over this point.
 * 2. When the pointer is moved, compute the new parent translation to keep the pointer on the same place on the
 *    dragged element.
 * 3. (optionally) map that to a model position, and (optionally) move that model position to satisfy any constraints of
 *    where the element can be dragged (recomputing the parent/model translation as needed)
 * 4. Apply the required translation (with a provided drag callback, using the positionProperty, or directly
 *    transforming the Node if translateNode:true).
 *
 * For example usage, see scenery/examples/input.html
 *
 * For most PhET model-view usage, it's recommended to include a model position Property as the `positionProperty`
 * option, along with the `transform` option specifying the MVT. By default, this will then assume that the Node with
 * the listener is positioned in the "view" coordinate frame, and will properly handle offsets and transformations.
 * It is assumed that when the model `positionProperty` changes, that the position of the Node would also change.
 * If it's another Node being transformed, please use the `targetNode` option to specify which Node is being
 * transformed. If something more complicated than a Node being transformed is going on (like positioning multiple
 * items, positioning based on the center, changing something in CanvasNode), it's recommended to pass the
 * `useParentOffset` option (so that the DragListener will NOT try to compute offsets based on the Node's position), or
 * to use `applyOffset:false` (effectively having drags reposition the Node so that the origin is at the pointer).
 *
 * The typical PhET usage would look like:
 *
 *   new DragListener( {
 *     positionProperty: someObject.positionProperty,
 *     transform: modelViewTransform
 *   } )
 *
 * Additionally, for PhET usage it's also fine NOT to hook into a `positionProperty`. Typically using start/end/drag,
 * and values can be read out (like `modelPoint`, `localPoint`, `parentPoint`, `modelDelta`) from the listener to do
 * operations. For instance, if deltas and model positions are the only thing desired:
 *
 *   new DragListener( {
 *     drag: ( event, listener ) => {
 *       doSomethingWith( listener.modelDelta, listener.modelPoint );
 *     }
 *   } )
 *
 * It's completely fine to use one DragListener with multiple objects, however this isn't done as much since specifying
 * positionProperty only works with ONE model position Property (so if things are backed by the same Property it would
 * be fine). Doing things based on modelPoint/modelDelta/etc. should be completely fine using one listener with
 * multiple nodes. The typical pattern IS creating one DragListener per draggable view Node.
 *
 * @author Jonathan Olson <jonathan.olson@colorado.edu>
 */
import PhetioAction from '../../../tandem/js/PhetioAction.js';
import Transform3 from '../../../dot/js/Transform3.js';
import Vector2 from '../../../dot/js/Vector2.js';
import optionize from '../../../phet-core/js/optionize.js';
import EventType from '../../../tandem/js/EventType.js';
import PhetioObject from '../../../tandem/js/PhetioObject.js';
import Tandem from '../../../tandem/js/Tandem.js';
import { PressListener, scenery, SceneryEvent, TransformTracker } from '../imports.js';
import Property from '../../../axon/js/Property.js';
// Scratch vectors used to prevent allocations
const scratchVector2A = new Vector2(0, 0);
const isPressedListener = (listener) => listener.isPressed;
export default class DragListener extends PressListener {
    constructor(providedOptions) {
        const options = optionize()({
            positionProperty: null,
            // start is preferred over passing press(), as the drag start hasn't been fully processed at that point.
            start: null,
            // end is preferred over passing release(), as the drag start hasn't been fully processed at that point.
            end: null,
            drag: _.noop,
            transform: null,
            dragBoundsProperty: null,
            allowTouchSnag: true,
            applyOffset: true,
            useParentOffset: false,
            trackAncestors: false,
            translateNode: false,
            mapPosition: null,
            offsetPosition: null,
            canClick: false,
            tandem: Tandem.REQUIRED,
            // Though DragListener is not instrumented, declare these here to support properly passing this to children, see https://github.com/phetsims/tandem/issues/60.
            // DragListener by default doesn't allow PhET-iO to trigger drag Action events
            phetioReadOnly: true,
            phetioFeatured: PhetioObject.DEFAULT_OPTIONS.phetioFeatured
        }, providedOptions);
        assert && assert(!options.dragBounds, 'options.dragBounds was removed in favor of options.dragBoundsProperty');
        assert && assert(!options.useParentOffset || options.positionProperty, 'If useParentOffset is set, a positionProperty is required');
        // @ts-expect-error TODO: See https://github.com/phetsims/phet-core/issues/128
        super(options);
        this._allowTouchSnag = options.allowTouchSnag;
        this._applyOffset = options.applyOffset;
        this._useParentOffset = options.useParentOffset;
        this._trackAncestors = options.trackAncestors;
        this._translateNode = options.translateNode;
        this._transform = options.transform;
        this._positionProperty = options.positionProperty;
        this._mapPosition = options.mapPosition;
        this._offsetPosition = options.offsetPosition;
        this._dragBoundsProperty = (options.dragBoundsProperty || new Property(null));
        this._start = options.start;
        this._end = options.end;
        this._canClick = options.canClick;
        this.isUserControlledProperty = this.isPressedProperty;
        this._globalPoint = new Vector2(0, 0);
        this._localPoint = new Vector2(0, 0);
        this._parentPoint = new Vector2(0, 0);
        this._modelPoint = new Vector2(0, 0);
        this._modelDelta = new Vector2(0, 0);
        this._parentOffset = new Vector2(0, 0);
        this._transformTracker = null;
        this._transformTrackerListener = this.ancestorTransformed.bind(this);
        this._lastInterruptedTouchLikePointer = null;
        this._dragAction = new PhetioAction(event => {
            assert && assert(isPressedListener(this));
            const pressedListener = this;
            const point = pressedListener.pointer.point;
            if (point) {
                // This is done first, before the drag listener is called (from the prototype drag call)
                if (!this._globalPoint.equals(point)) {
                    this.reposition(point);
                }
            }
            PressListener.prototype.drag.call(this, event);
        }, {
            parameters: [{ name: 'event', phetioType: SceneryEvent.SceneryEventIO }],
            phetioFeatured: options.phetioFeatured,
            tandem: options.tandem.createTandem('dragAction'),
            phetioHighFrequency: true,
            phetioDocumentation: 'Emits whenever a drag occurs with an SceneryEventIO argument.',
            phetioReadOnly: options.phetioReadOnly,
            phetioEventType: EventType.USER
        });
    }
    /**
     * Attempts to start a drag with a press.
     *
     * NOTE: This is safe to call externally in order to attempt to start a press. dragListener.canPress( event ) can
     * be used to determine whether this will actually start a drag.
     *
     * @param event
     * @param [targetNode] - If provided, will take the place of the targetNode for this call. Useful for forwarded presses.
     * @param [callback] - to be run at the end of the function, but only on success
     * @returns success - Returns whether the press was actually started
     */
    press(event, targetNode, callback) {
        sceneryLog && sceneryLog.InputListener && sceneryLog.InputListener('DragListener press');
        sceneryLog && sceneryLog.InputListener && sceneryLog.push();
        const success = super.press(event, targetNode, () => {
            sceneryLog && sceneryLog.InputListener && sceneryLog.InputListener('DragListener successful press');
            sceneryLog && sceneryLog.InputListener && sceneryLog.push();
            assert && assert(isPressedListener(this));
            const pressedListener = this;
            // signify that this listener is reserved for dragging so that other listeners can change
            // their behavior during scenery event dispatch
            pressedListener.pointer.reserveForDrag();
            this.attachTransformTracker();
            assert && assert(pressedListener.pointer.point !== null);
            const point = pressedListener.pointer.point;
            // Compute the parent point corresponding to the pointer's position
            const parentPoint = this.globalToParentPoint(this._localPoint.set(point));
            if (this._useParentOffset) {
                this.modelToParentPoint(this._parentOffset.set(this._positionProperty.value)).subtract(parentPoint);
            }
            // Set the local point
            this.parentToLocalPoint(parentPoint);
            this.reposition(point);
            // Notify after positioning and other changes
            this._start && this._start(event, pressedListener);
            callback && callback();
            sceneryLog && sceneryLog.InputListener && sceneryLog.pop();
        });
        sceneryLog && sceneryLog.InputListener && sceneryLog.pop();
        return success;
    }
    /**
     * Stops the drag.
     *
     * This can be called from the outside to stop the drag without the pointer having actually fired any 'up'
     * events. If the cancel/interrupt behavior is more preferable, call interrupt() on this listener instead.
     *
     * @param [event] - scenery event if there was one
     * @param [callback] - called at the end of the release
     */
    release(event, callback) {
        sceneryLog && sceneryLog.InputListener && sceneryLog.InputListener('DragListener release');
        sceneryLog && sceneryLog.InputListener && sceneryLog.push();
        super.release(event, () => {
            this.detachTransformTracker();
            // Notify after the rest of release is called in order to prevent it from triggering interrupt().
            this._end && this._end(event || null, this);
            callback && callback();
        });
        sceneryLog && sceneryLog.InputListener && sceneryLog.pop();
    }
    /**
     * Components using DragListener should generally not be activated with a click. A single click from alternative
     * input would pick up the component then immediately release it. But occasionally that is desirable and can be
     * controlled with the canClick option.
     */
    canClick() {
        return super.canClick() && this._canClick;
    }
    /**
     * Activate the DragListener with a click activation. Usually, DragListener will NOT be activated with a click
     * and canClick will return false. Components that can be dragged usually should not be picked up/released
     * from a single click event that may have even come from event bubbling. But it can be optionally allowed for some
     * components that have drag functionality but can still be activated with a single click event.
     * (scenery-internal) (part of the scenery listener API)
     */
    click(event, callback) {
        sceneryLog && sceneryLog.InputListener && sceneryLog.InputListener('DragListener click');
        sceneryLog && sceneryLog.InputListener && sceneryLog.push();
        const success = super.click(event, () => {
            sceneryLog && sceneryLog.InputListener && sceneryLog.InputListener('DragListener successful press');
            sceneryLog && sceneryLog.InputListener && sceneryLog.push();
            // notify that we have started a change
            this._start && this._start(event, this);
            callback && callback();
            // notify that we have finished a 'drag' activation through click
            this._end && this._end(event, this);
            sceneryLog && sceneryLog.InputListener && sceneryLog.pop();
        });
        sceneryLog && sceneryLog.InputListener && sceneryLog.pop();
        return success;
    }
    /**
     * Called when move events are fired on the attached pointer listener during a drag.
     */
    drag(event) {
        assert && assert(isPressedListener(this));
        const pressedListener = this;
        const point = pressedListener.pointer.point;
        // Ignore global moves that have zero length (Chrome might autofire, see
        // https://code.google.com/p/chromium/issues/detail?id=327114)
        if (!point || this._globalPoint.equals(point)) {
            return;
        }
        // If we got interrupted while events were queued up, we MAY get a drag when not pressed. We can ignore this.
        if (!this.isPressed) {
            return;
        }
        sceneryLog && sceneryLog.InputListener && sceneryLog.InputListener('DragListener drag');
        sceneryLog && sceneryLog.InputListener && sceneryLog.push();
        this._dragAction.execute(event);
        sceneryLog && sceneryLog.InputListener && sceneryLog.pop();
    }
    /**
     * Attempts to start a touch snag, given a SceneryEvent.
     *
     * Should be safe to be called externally with an event.
     */
    tryTouchSnag(event) {
        sceneryLog && sceneryLog.InputListener && sceneryLog.InputListener('DragListener tryTouchSnag');
        sceneryLog && sceneryLog.InputListener && sceneryLog.push();
        if (this._allowTouchSnag && (!this.attach || !event.pointer.isAttached())) {
            this.press(event);
        }
        sceneryLog && sceneryLog.InputListener && sceneryLog.pop();
    }
    /**
     * Returns a defensive copy of the local-coordinate-frame point of the drag.
     */
    getGlobalPoint() {
        return this._globalPoint.copy();
    }
    get globalPoint() { return this.getGlobalPoint(); }
    /**
     * Returns a defensive copy of the local-coordinate-frame point of the drag.
     */
    getLocalPoint() {
        return this._localPoint.copy();
    }
    get localPoint() { return this.getLocalPoint(); }
    /**
     * Returns a defensive copy of the parent-coordinate-frame point of the drag.
     */
    getParentPoint() {
        return this._parentPoint.copy();
    }
    get parentPoint() { return this.getParentPoint(); }
    /**
     * Returns a defensive copy of the model-coordinate-frame point of the drag.
     */
    getModelPoint() {
        return this._modelPoint.copy();
    }
    get modelPoint() { return this.getModelPoint(); }
    /**
     * Returns a defensive copy of the model-coordinate-frame delta.
     */
    getModelDelta() {
        return this._modelDelta.copy();
    }
    get modelDelta() { return this.getModelDelta(); }
    /**
     * Maps a point from the global coordinate frame to our drag target's parent coordinate frame.
     *
     * NOTE: This mutates the input vector (for performance)
     *
     * Should be overridden if a custom transformation is needed.
     */
    globalToParentPoint(globalPoint) {
        assert && assert(isPressedListener(this));
        const pressedListener = this;
        let referenceResult;
        if (assert) {
            referenceResult = pressedListener.pressedTrail.globalToParentPoint(globalPoint);
        }
        pressedListener.pressedTrail.getParentTransform().getInverse().multiplyVector2(globalPoint);
        assert && assert(globalPoint.equals(referenceResult));
        return globalPoint;
    }
    /**
     * Maps a point from the drag target's parent coordinate frame to its local coordinate frame.
     *
     * NOTE: This mutates the input vector (for performance)
     *
     * Should be overridden if a custom transformation is needed.
     */
    parentToLocalPoint(parentPoint) {
        assert && assert(isPressedListener(this));
        const pressedListener = this;
        let referenceResult;
        if (assert) {
            referenceResult = pressedListener.pressedTrail.lastNode().parentToLocalPoint(parentPoint);
        }
        pressedListener.pressedTrail.lastNode().getTransform().getInverse().multiplyVector2(parentPoint);
        assert && assert(parentPoint.equals(referenceResult));
        return parentPoint;
    }
    /**
     * Maps a point from the drag target's local coordinate frame to its parent coordinate frame.
     *
     * NOTE: This mutates the input vector (for performance)
     *
     * Should be overridden if a custom transformation is needed.
     */
    localToParentPoint(localPoint) {
        assert && assert(isPressedListener(this));
        const pressedListener = this;
        let referenceResult;
        if (assert) {
            referenceResult = pressedListener.pressedTrail.lastNode().localToParentPoint(localPoint);
        }
        pressedListener.pressedTrail.lastNode().getMatrix().multiplyVector2(localPoint);
        assert && assert(localPoint.equals(referenceResult));
        return localPoint;
    }
    /**
     * Maps a point from the drag target's parent coordinate frame to the model coordinate frame.
     *
     * NOTE: This mutates the input vector (for performance)
     *
     * Should be overridden if a custom transformation is needed. Note that by default, unless a transform is provided,
     * the parent coordinate frame will be the same as the model coordinate frame.
     */
    parentToModelPoint(parentPoint) {
        if (this._transform) {
            const transform = this._transform instanceof Transform3 ? this._transform : this._transform.value;
            transform.getInverse().multiplyVector2(parentPoint);
        }
        return parentPoint;
    }
    /**
     * Maps a point from the model coordinate frame to the drag target's parent coordinate frame.
     *
     * NOTE: This mutates the input vector (for performance)
     *
     * Should be overridden if a custom transformation is needed. Note that by default, unless a transform is provided,
     * the parent coordinate frame will be the same as the model coordinate frame.
     */
    modelToParentPoint(modelPoint) {
        if (this._transform) {
            const transform = this._transform instanceof Transform3 ? this._transform : this._transform.value;
            transform.getMatrix().multiplyVector2(modelPoint);
        }
        return modelPoint;
    }
    /**
     * Apply a mapping from the drag target's model position to an allowed model position.
     *
     * A common example is using dragBounds, where the position of the drag target is constrained to within a bounding
     * box. This is done by mapping points outside the bounding box to the closest position inside the box. More
     * general mappings can be used.
     *
     * Should be overridden (or use mapPosition) if a custom transformation is needed.
     *
     * @returns - A point in the model coordinate frame
     */
    mapModelPoint(modelPoint) {
        if (this._mapPosition) {
            modelPoint = this._mapPosition(modelPoint);
        }
        if (this._dragBoundsProperty.value) {
            return this._dragBoundsProperty.value.closestPointTo(modelPoint);
        }
        else {
            return modelPoint;
        }
    }
    /**
     * Mutates the parentPoint given to account for the initial pointer's offset from the drag target's origin.
     */
    applyParentOffset(parentPoint) {
        if (this._offsetPosition) {
            parentPoint.add(this._offsetPosition(parentPoint, this));
        }
        // Don't apply any offset if applyOffset is false
        if (this._applyOffset) {
            if (this._useParentOffset) {
                parentPoint.add(this._parentOffset);
            }
            else {
                // Add the difference between our local origin (in the parent coordinate frame) and the local point (in the same
                // parent coordinate frame).
                parentPoint.subtract(this.localToParentPoint(scratchVector2A.set(this._localPoint)));
                parentPoint.add(this.localToParentPoint(scratchVector2A.setXY(0, 0)));
            }
        }
    }
    /**
     * Triggers an update of the drag position, potentially changing position properties.
     *
     * Should be called when something that changes the output positions of the drag occurs (most often, a drag event
     * itself).
     */
    reposition(globalPoint) {
        sceneryLog && sceneryLog.InputListener && sceneryLog.InputListener('DragListener reposition');
        sceneryLog && sceneryLog.InputListener && sceneryLog.push();
        assert && assert(isPressedListener(this));
        const pressedListener = this;
        this._globalPoint.set(globalPoint);
        // Update parentPoint mutably.
        this.applyParentOffset(this.globalToParentPoint(this._parentPoint.set(globalPoint)));
        // To compute the delta (new - old), we first mutate it to (-old)
        this._modelDelta.set(this._modelPoint).negate();
        // Compute the modelPoint from the parentPoint
        this._modelPoint.set(this.mapModelPoint(this.parentToModelPoint(scratchVector2A.set(this._parentPoint))));
        // Complete the delta computation
        this._modelDelta.add(this._modelPoint);
        // Apply any mapping changes back to the parent point
        this.modelToParentPoint(this._parentPoint.set(this._modelPoint));
        if (this._translateNode) {
            pressedListener.pressedTrail.lastNode().translation = this._parentPoint;
        }
        if (this._positionProperty) {
            this._positionProperty.value = this._modelPoint.copy(); // Include an extra reference so that it will change.
        }
        sceneryLog && sceneryLog.InputListener && sceneryLog.pop();
    }
    /**
     * Called with 'touchenter' events (part of the listener API). (scenery-internal)
     *
     * NOTE: Do not call directly. See the press method instead.
     */
    touchenter(event) {
        if (event.pointer.isDownProperty.value) {
            this.tryTouchSnag(event);
        }
    }
    /**
     * Called with 'touchmove' events (part of the listener API). (scenery-internal)
     *
     * NOTE: Do not call directly. See the press method instead.
     */
    touchmove(event) {
        this.tryTouchSnag(event);
    }
    /**
     * Called when an ancestor's transform has changed (when trackAncestors is true).
     */
    ancestorTransformed() {
        assert && assert(isPressedListener(this));
        const pressedListener = this;
        const point = pressedListener.pointer.point;
        if (point) {
            // Reposition based on the current point.
            this.reposition(point);
        }
    }
    /**
     * Attaches our transform tracker (begins listening to the ancestor transforms)
     */
    attachTransformTracker() {
        assert && assert(isPressedListener(this));
        const pressedListener = this;
        if (this._trackAncestors) {
            this._transformTracker = new TransformTracker(pressedListener.pressedTrail.copy().removeDescendant());
            this._transformTracker.addListener(this._transformTrackerListener);
        }
    }
    /**
     * Detaches our transform tracker (stops listening to the ancestor transforms)
     */
    detachTransformTracker() {
        if (this._transformTracker) {
            this._transformTracker.removeListener(this._transformTrackerListener);
            this._transformTracker.dispose();
            this._transformTracker = null;
        }
    }
    /**
     * Returns the drag bounds of the listener.
     */
    getDragBounds() {
        return this._dragBoundsProperty.value;
    }
    get dragBounds() { return this.getDragBounds(); }
    /**
     * Sets the drag transform of the listener.
     */
    setTransform(transform) {
        this._transform = transform;
    }
    set transform(transform) { this.setTransform(transform); }
    get transform() { return this.getTransform(); }
    /**
     * Returns the transform of the listener.
     */
    getTransform() {
        return this._transform;
    }
    /**
     * Interrupts the listener, releasing it (canceling behavior).
     *
     * This effectively releases/ends the press, and sets the `interrupted` flag to true while firing these events
     * so that code can determine whether a release/end happened naturally, or was canceled in some way.
     *
     * This can be called manually, but can also be called through node.interruptSubtreeInput().
     */
    interrupt() {
        if (this.pointer && this.pointer.isTouchLike()) {
            this._lastInterruptedTouchLikePointer = this.pointer;
        }
        super.interrupt();
    }
    /**
     * Returns whether a press can be started with a particular event.
     */
    canPress(event) {
        if (event.pointer === this._lastInterruptedTouchLikePointer) {
            return false;
        }
        return super.canPress(event);
    }
    /**
     * Disposes the listener, releasing references. It should not be used after this.
     */
    dispose() {
        sceneryLog && sceneryLog.InputListener && sceneryLog.InputListener('DragListener dispose');
        sceneryLog && sceneryLog.InputListener && sceneryLog.push();
        this._dragAction.dispose();
        this.detachTransformTracker();
        super.dispose();
        sceneryLog && sceneryLog.InputListener && sceneryLog.pop();
    }
    /**
     * Creates an input listener that forwards events to the specified input listener. The target listener should
     * probably be using PressListener.options.targetNode so that the forwarded drag has the correct Trail
     *
     * See https://github.com/phetsims/scenery/issues/639
     */
    static createForwardingListener(down, providedOptions) {
        const options = optionize()({
            allowTouchSnag: true // see https://github.com/phetsims/scenery/issues/999
        }, providedOptions);
        return {
            down(event) {
                if (event.canStartPress()) {
                    down(event);
                }
            },
            touchenter(event) {
                options.allowTouchSnag && this.down(event);
            },
            touchmove(event) {
                options.allowTouchSnag && this.down(event);
            }
        };
    }
}
scenery.register('DragListener', DragListener);
//# sourceMappingURL=DragListener.js.map