import '../../assert/js/assert.js';
import '../../globals.js';
import { isTReadOnlyProperty } from '../../axon/js/TReadOnlyProperty.js';
import optionize from '../../phet-core/js/optionize.js';
import Easing from './Easing.js';
import twixt from './twixt.js';
class AnimationTarget {
    /**
     * NOTE: Generally don't use this directly. Instead, use Animation, providing config for one or more targets.
     *
     * Every animation target needs two things:
     *
     * 1. A way of getting/setting the animated value (`setValue`/`getValue`, `property`, or `object`/`attribute`).
     * 2. A way of determining the value to animate toward (`to` or `delta`).
     */
    constructor(providedConfig) {
        // Computed start/end values for the animation (once the animation finishes the delay and begins)
        this.startingValue = null;
        this.endingValue = null;
        const config = optionize()({
            setValue: null,
            getValue: null,
            property: null,
            object: null,
            attribute: null,
            to: null,
            delta: null,
            speed: null,
            from: null,
            easing: Easing.CUBIC_IN_OUT,
            // @ts-expect-error - Not sure how to tell it that the default doesn't work for some types, since we can't use the trickier object
            blend: AnimationTarget.DEFAULT_BLEND,
            // @ts-expect-error - Not sure how to tell it that the default doesn't work for some types, since we can't use the trickier object
            distance: AnimationTarget.DEFAULT_DISTANCE,
            // @ts-expect-error - Not sure how to tell it that the default doesn't work for some types, since we can't use the trickier object
            add: AnimationTarget.DEFAULT_ADD
        }, providedConfig);
        assert && assert(+(config.property !== null) + +(config.object !== null) + +(config.setValue !== null) === 1, 'Should have one (and only one) way of defining how to set the animated value. Use one of property/object/setValue');
        assert && assert(config.setValue === null || typeof config.setValue === 'function', 'If setValue is provided, it should be a function.');
        assert && assert(config.setValue === null || config.from !== null || typeof config.getValue === 'function', 'If setValue is provided and no "from" value is specified, then getValue needs to be a function.');
        assert && assert(config.to !== null || config.delta !== null, 'Need something to animate to, use to/delta');
        assert && assert(config.property === null ||
            ((isTReadOnlyProperty(config.property)) && config.property.isSettable()), 'If property is provided, it should be a settable Property or TinyProperty');
        assert && assert(config.object === null || (typeof config.object === 'object' && typeof config.attribute === 'string'), 'If object is provided, then object should be an object, and attribute should be a string.');
        assert && assert(config.easing instanceof Easing, 'The easing should be of type Easing');
        assert && assert(typeof config.blend === 'function', 'The blend option should be a function');
        assert && assert(typeof config.distance === 'function', 'The distance option should be a function');
        assert && assert(typeof config.add === 'function', 'The add option should be a function');
        // If `object` is provided, create the associated getter/setter
        if (config.object) {
            assert && assert(config.attribute !== null);
            // @ts-expect-error - We know that config.object is an object, but TS doesn't
            config.setValue = AnimationTarget.OBJECT_SET(config.object, config.attribute);
            // @ts-expect-error - We know that config.object is an object, but TS doesn't
            config.getValue = AnimationTarget.OBJECT_GET(config.object, config.attribute);
        }
        // If `property` is provided, create the associated getter/setter
        if (config.property) {
            config.setValue = AnimationTarget.PROPERTY_SET(config.property);
            config.getValue = AnimationTarget.PROPERTY_GET(config.property);
        }
        assert && assert(config.setValue !== null);
        this.getValue = config.getValue;
        this.setValue = config.setValue;
        this.easing = config.easing;
        this.from = config.from;
        this.to = config.to;
        this.delta = config.delta;
        this.speed = config.speed; // Saved config to help determine the length of the animation
        this.blend = config.blend;
        this.distance = config.distance;
        this.add = config.add;
    }
    static DEFAULT_BLEND(a, b, ratio) {
        assert && assert(isFinite(ratio) && ratio >= 0 && ratio <= 1, `Invalid ratio: ${ratio}`);
        if (ratio === 0) {
            return a;
        }
        if (ratio === 1) {
            return b;
        }
        if (typeof a === 'number' && typeof b === 'number') {
            // @ts-expect-error - It can't detect we're in the T === number case.
            return a + (b - a) * ratio;
        }
        if (typeof a === 'object' && typeof b === 'object' && typeof a.blend === 'function') {
            return a.blend(b, ratio);
        }
        throw new Error(`Blending not supported for: ${JSON.stringify(a)}, ${JSON.stringify(b)}, pass in a blend option`);
    }
    static DEFAULT_DISTANCE(a, b) {
        if (typeof a === 'number' && typeof b === 'number') {
            return Math.abs(a - b);
        }
        if (typeof a === 'object' && typeof b === 'object' && typeof a.distance === 'function') {
            return a.distance(b);
        }
        throw new Error(`Distance (required for speed) by default not supported for: ${JSON.stringify(a)}, ${JSON.stringify(b)}, pass in a distance option`);
    }
    static DEFAULT_ADD(a, b) {
        if (typeof a === 'number' && typeof b === 'number') {
            // @ts-expect-error - It can't detect we're in the T === number case.
            return a + b;
        }
        if (typeof a === 'object' && typeof b === 'object' && typeof a.plus === 'function') {
            return a.plus(b);
        }
        throw new Error(`Addition (required for delta) by default not supported for: ${JSON.stringify(a)}, ${JSON.stringify(b)}, pass in an add option`);
    }
    /**
     * Helper function for creating a setter closure for object[ attribute ].
     */
    static OBJECT_SET(object, attribute) {
        return function (value) {
            object[attribute] = value;
        };
    }
    /**
     * Helper function for creating a getter closure for object[ attribute ].
     */
    static OBJECT_GET(object, attribute) {
        return function () {
            return object[attribute];
        };
    }
    /**
     * Helper function for creating a setter closure for Properties
     */
    static PROPERTY_SET(property) {
        return function (value) {
            property.value = value;
        };
    }
    /**
     * Helper function for creating a getter closure for Properties
     */
    static PROPERTY_GET(property) {
        return function () {
            return property.value;
        };
    }
    /**
     * Computes the starting and ending values of this target.
     *
     * Generally called when the animation is just about to begin, so it can look up the current value if necessary.
     */
    computeStartEnd() {
        assert && assert(this.to !== null || this.delta !== null);
        assert && assert(this.from !== null || this.getValue !== null);
        this.startingValue = (this.from !== null) ? this.from : this.getValue();
        this.endingValue = (this.to !== null) ? this.to : this.add(this.startingValue, this.delta);
    }
    /**
     * Updates the value of this target.
     *
     * @param ratio - How far along (from 0 to 1) in the animation.
     */
    update(ratio) {
        // These should be non-null by this point
        assert && assert(this.setValue !== null);
        assert && assert(this.startingValue !== null);
        assert && assert(this.endingValue !== null);
        this.setValue(this.blend(this.startingValue, this.endingValue, this.easing.value(ratio)));
    }
    /**
     * Whether this target can define the duration of an animation.
     */
    hasPreferredDuration() {
        return this.speed !== null;
    }
    /**
     * Returns the preferred duration of this target (or null if not defined).
     */
    getPreferredDuration() {
        assert && assert(this.startingValue !== null);
        assert && assert(this.delta !== null);
        return this.speed === null ? null : this.speed * this.distance(this.startingValue, this.delta);
    }
}
twixt.register('AnimationTarget', AnimationTarget);
export default AnimationTarget;
//# sourceMappingURL=AnimationTarget.js.map