import _ from 'lodash';
import '../../assert/js/assert.js';
import '../../globals.js';
// Copyright 2013-2024, University of Colorado Boulder
/**
 * A DerivedProperty is computed based on other Properties.  This implementation inherits from Property to (a) simplify
 * implementation and (b) ensure it remains consistent. Note that the setters should not be called directly, so the
 * setters (set, reset and es5 setter) throw an error if used directly.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 */
import optionize from '../../phet-core/js/optionize.js';
import IOTypeCache from '../../tandem/js/IOTypeCache.js';
import PhetioObject from '../../tandem/js/PhetioObject.js';
import Tandem from '../../tandem/js/Tandem.js';
import IOType from '../../tandem/js/types/IOType.js';
import VoidIO from '../../tandem/js/types/VoidIO.js';
import axon from './axon.js';
import Property from './Property.js';
import { propertyStateHandlerSingleton } from './PropertyStateHandler.js';
import PropertyStatePhase from './PropertyStatePhase.js';
import ReadOnlyProperty from './ReadOnlyProperty.js';
const DERIVED_PROPERTY_IO_PREFIX = 'DerivedPropertyIO';
/**
 * Compute the derived value given a derivation and an array of dependencies
 */
function getDerivedValue(derivation, dependencies) {
    // @ts-expect-error
    return derivation(...dependencies.map(property => property.get()));
}
/**
 * T = type of the derived value
 * Parameters[] = types of the callback parameters, e.g. [ Vector2, number, boolean ]
 */
export default class DerivedProperty extends ReadOnlyProperty {
    constructor(dependencies, derivation, providedOptions) {
        const options = optionize()({
            phetioReadOnly: true, // derived properties can be read but not set by PhET-iO
            phetioOuterType: DerivedProperty.DerivedPropertyIO,
            phetioLinkDependencies: true
        }, providedOptions);
        assert && assert(dependencies.every(_.identity), 'dependencies should all be truthy');
        assert && assert(dependencies.length === _.uniq(dependencies).length, 'duplicate dependencies');
        const initialValue = getDerivedValue(derivation, dependencies);
        // We must pass supertype tandem to parent class so addInstance is called only once in the subclassiest constructor.
        super(initialValue, options);
        if (Tandem.VALIDATION && this.isPhetioInstrumented()) {
            // The phetioType should be a concrete (instantiated) DerivedPropertyIO, hence we must check its outer type
            assert && assert(this.phetioType.typeName.startsWith('DerivedPropertyIO'), 'phetioType should be DerivedPropertyIO');
        }
        this.dependencies = dependencies;
        this.derivation = derivation;
        this.derivedPropertyListener = this.getDerivedPropertyListener.bind(this);
        dependencies.forEach(dependency => {
            dependency.lazyLink(this.derivedPropertyListener);
            if (Tandem.PHET_IO_ENABLED && this.isPhetioInstrumented() && dependency instanceof PhetioObject && dependency.isPhetioInstrumented()) {
                if (dependency instanceof ReadOnlyProperty) {
                    // Dependencies should have taken their correct values before this DerivedProperty undefers, so it will be sure
                    // to have the right value.
                    // NOTE: Do not mark the beforePhase as NOTIFY, as this will potentially cause interdependence bugs when used
                    // with Multilinks. See Projectile Motion's use of MeasuringTapeNode for an example.
                    propertyStateHandlerSingleton.registerPhetioOrderDependency(dependency, PropertyStatePhase.UNDEFER, this, PropertyStatePhase.UNDEFER);
                }
                if (options.tandem && options.phetioLinkDependencies) {
                    const dependenciesTandem = options.tandem.createTandem('dependencies');
                    this.addLinkedElement(dependency, {
                        tandem: dependenciesTandem.createTandemFromPhetioID(dependency.tandem.phetioID)
                    });
                }
            }
        });
    }
    /**
     * Determines whether this DerivedProperty has a specific dependency.
     */
    hasDependency(dependency) {
        return this.definedDependencies.includes(dependency);
    }
    /**
     * Returns dependencies that are guaranteed to be defined internally.
     */
    get definedDependencies() {
        assert && assert(this.dependencies !== null, 'Dependencies should be defined, has this Property been disposed?');
        return this.dependencies;
    }
    // for bind
    getDerivedPropertyListener() {
        // Don't try to recompute if we are disposed, see https://github.com/phetsims/axon/issues/432
        if (this.isDisposed) {
            return;
        }
        // Just mark that there is a deferred value, then calculate the derivation below when setDeferred() is called.
        // This is in part supported by the PhET-iO state engine because it can account for intermediate states, such
        // that this Property won't notify until after it is undeferred and has taken its final value.
        if (this.isDeferred) {
            this.hasDeferredValue = true;
        }
        else {
            super.set(getDerivedValue(this.derivation, this.definedDependencies));
        }
    }
    /**
     * Allows forcing a recomputation (as a possible workaround to listener order). This works well if you have a
     * non-Property event that should trigger a value change for this Property.
     *
     * For example:
     * myEmitter.addListener( () => myDerivedProperty.recomputeDerivation() );
     * myObservableArray.addItemAddedListener( () => myDerivedProperty.recomputeDerivation() );
     */
    recomputeDerivation() {
        this.getDerivedPropertyListener();
    }
    dispose() {
        const dependencies = this.definedDependencies;
        // Unlink from dependent Properties
        for (let i = 0; i < dependencies.length; i++) {
            const dependency = dependencies[i];
            if (dependency.hasListener(this.derivedPropertyListener)) {
                dependency.unlink(this.derivedPropertyListener);
            }
        }
        this.dependencies = null;
        super.dispose();
    }
    /**
     * Support deferred DerivedProperty by only calculating the derivation once when it is time to undefer it and fire
     * notifications. This way we don't have intermediate derivation calls during PhET-iO state setting.
     */
    setDeferred(isDeferred) {
        if (this.isDeferred && !isDeferred) {
            this.deferredValue = getDerivedValue(this.derivation, this.definedDependencies);
        }
        return super.setDeferred(isDeferred);
    }
    /**
     * Creates a derived boolean Property whose value is true iff firstProperty's value is equal to secondProperty's
     * value.
     */
    static valueEquals(firstProperty, secondProperty, options) {
        return new DerivedProperty([firstProperty, secondProperty], (u, v) => u === v, options);
    }
    /**
     * Creates a derived boolean Property whose value is true iff firstProperty's value is not equal to the
     * secondProperty's value.
     */
    static valueNotEquals(firstProperty, secondProperty, options) {
        return new DerivedProperty([firstProperty, secondProperty], (u, v) => u !== v, options);
    }
    /**
     * Creates a derived boolean Property whose value is true iff firstProperty's value is equal to a constant value.
     */
    static valueEqualsConstant(firstProperty, value, options) {
        return new DerivedProperty([firstProperty], (u) => u === value, options);
    }
    /**
     * Creates a derived boolean Property whose value is true iff firstProperty's value is not equal to a constant value.
     */
    static valueNotEqualsConstant(firstProperty, value, options) {
        return new DerivedProperty([firstProperty], (u) => u !== value, options);
    }
    /**
     * Creates a derived boolean Property whose value is true iff every input Property value is true.
     */
    static and(properties, options) {
        assert && assert(properties.length > 0, 'must provide a dependency');
        return DerivedProperty.deriveAny(properties, () => _.reduce(properties, andFunction, true), options);
    }
    /**
     * Creates a derived boolean Property whose value is true iff any input Property value is true.
     */
    static or(properties, options) {
        assert && assert(properties.length > 0, 'must provide a dependency');
        return DerivedProperty.deriveAny(properties, () => _.reduce(properties, orFunction, false), options);
    }
    /**
     * Creates a derived number Property whose value is the result of multiplying all (number) dependencies together.
     */
    static multiply(properties, options) {
        assert && assert(properties.length > 0, 'must provide a dependency');
        return DerivedProperty.deriveAny(properties, () => _.reduce(properties, multiplyFunction, 1), options);
    }
    /**
     * Creates a derived number Property whose value is the result of adding all (number) dependencies together.
     */
    static add(properties, options) {
        assert && assert(properties.length > 0, 'must provide a dependency');
        return DerivedProperty.deriveAny(properties, () => _.reduce(properties, addFunction, 0), options);
    }
    /**
     * Creates a derived boolean Property whose value is the inverse of the provided property.
     */
    static not(propertyToInvert, options) {
        return new DerivedProperty([propertyToInvert], (x) => !x, options);
    }
    /**
     * Create a DerivedProperty from any number of dependencies.  This is parallel to Multilink.multilinkAny
     */
    static deriveAny(dependencies, derivation, providedOptions) {
        return new DerivedProperty(
        // @ts-expect-error we have to provide a mapping between an arbitrary length array and our max overload of 15 types.
        dependencies, derivation, providedOptions);
    }
}
const andFunction = (value, property) => {
    return value && property.value;
};
const orFunction = (value, property) => {
    assert && assert(typeof property.value === 'boolean', 'boolean value required');
    return value || property.value;
};
const multiplyFunction = (value, property) => {
    assert && assert(typeof property.value === 'number', 'number value required');
    return value * property.value;
};
const addFunction = (value, property) => {
    assert && assert(typeof property.value === 'number', 'number value required');
    return value + property.value;
};
// Cache each parameterized DerivedPropertyIO so that it is only created once.
const cache = new IOTypeCache();
/**
 * Parametric IOType constructor.  Given a parameter type, this function returns an appropriate DerivedProperty
 * IOType. Unlike PropertyIO, DerivedPropertyIO cannot be set by PhET-iO clients.
 * This caching implementation should be kept in sync with the other parametric IOType caching implementations.
 */
DerivedProperty.DerivedPropertyIO = parameterType => {
    assert && assert(parameterType, 'DerivedPropertyIO needs parameterType');
    if (!cache.has(parameterType)) {
        cache.set(parameterType, new IOType(`${DERIVED_PROPERTY_IO_PREFIX}<${parameterType.typeName}>`, {
            valueType: DerivedProperty,
            parameterTypes: [parameterType],
            supertype: Property.PropertyIO(parameterType),
            documentation: 'Like PropertyIO, but not settable.  Instead it is derived from other DerivedPropertyIO or PropertyIO ' +
                'instances',
            // Override the parent implementation as a no-op.  DerivedProperty values appear in the state, but should not be set
            // back into a running simulation. See https://github.com/phetsims/phet-io/issues/1292
            applyState: _.noop,
            methods: {
                setValue: {
                    returnType: VoidIO,
                    parameterTypes: [parameterType],
                    // @ts-expect-error
                    implementation: DerivedProperty.prototype.set,
                    documentation: 'Errors out when you try to set a derived property.',
                    invocableForReadOnlyElements: false
                }
            }
        }));
    }
    return cache.get(parameterType);
};
// Convenience classes for subclassing DerivedProperty
export class DerivedProperty1 extends DerivedProperty {
}
export class DerivedProperty2 extends DerivedProperty {
}
export class DerivedProperty3 extends DerivedProperty {
}
export class DerivedProperty4 extends DerivedProperty {
}
export class DerivedProperty5 extends DerivedProperty {
}
axon.register('DerivedProperty', DerivedProperty);
//# sourceMappingURL=DerivedProperty.js.map