import '../../assert/js/assert.js';
import '../../tandem/js/PhetioIDUtils.js';
import '../../globals.js';
// Copyright 2022-2024, University of Colorado Boulder
/**
 * A DerivedProperty that applies a string pattern (like StringUtils.fillIn). It has options for mapping values,
 * either with decimalPlaces or an arbitrary function so that there is just one Property set up for disposal.
 *
 * PatternStringPropertyTests.ts has many examples that may be useful!
 *
 * Basic usage can involve values as Properties or directly as values (both work):
 *
 * | const patternStringProperty = new TinyProperty( '{{valueA}} + {{valueB}}' );
 * | const property = new PatternStringProperty( patternStringProperty, {
 * |   valueA: 5,
 * |   valueB: new TinyProperty( 7 )
 * | } );
 * | property.value; // '5 + 7'
 *
 * Did your string use StringUtils.format? formatNames can be used to be compatible:
 *
 * | const property = new PatternStringProperty( new TinyProperty( '{0} + {1}' ), {
 * |   valueA: 5,
 * |   valueB: new TinyProperty( 7 )
 * | }, {
 * |   formatNames: [ 'valueA', 'valueB' ]
 * | } );
 * | property.value; // '5 + 7'
 *
 * Want to apply unit conversions or other formulas? Use maps
 *
 * | const gramsProperty = new TinyProperty( 2000 );
 * | new PatternStringProperty( new TinyProperty( '{{kilograms}} kg' ), {
 * |   kilograms: gramsProperty
 * | }, {
 * |   maps: {
 * |     kilograms: ( grams: number ) => grams / 1000
 * |   }
 * | } );
 * | property.value; // '2 kg'
 *
 * Using a numeric value, and want to show a certain number of decimal places? Use decimalPlaces!
 *
 * | const gramsProperty = new TinyProperty( 2143 );
 * | new PatternStringProperty( new TinyProperty( '{{kilograms}} kg' ), {
 * |   kilograms: gramsProperty
 * | }, {
 * |   maps: {
 * |     kilograms: ( grams: number ) => grams / 1000
 * |   },
 * |   decimalPlaces: 2
 * | } );
 * | property.value; // '2.14 kg'
 *
 * @author Jonathan Olson <jonathan.olson@colorado.edu>
 */
import Utils from '../../dot/js/Utils.js';
import optionize from '../../phet-core/js/optionize.js';
import Tandem from '../../tandem/js/Tandem.js';
import axon from './axon.js';
import DerivedStringProperty from './DerivedStringProperty.js';
import { isTReadOnlyProperty } from './TReadOnlyProperty.js';
// Shared here, since it will always be the same function
const stringify = (value) => `${value}`;
export default class PatternStringProperty extends DerivedStringProperty {
    constructor(patternProperty, values, providedOptions) {
        assert && assert(!(values.tandem instanceof Tandem), 'Did you intend to put tandem in providedOptions?');
        const options = optionize()({
            formatNames: [],
            decimalPlaces: null,
            phetioFeatured: true, // This is the best default to match all i18n "model" strings, see https://github.com/phetsims/studio/issues/304#issuecomment-1572613118
            tandemNameSuffix: 'StringProperty'
        }, providedOptions);
        const dependencies = [patternProperty];
        const maps = {};
        const keys = Object.keys(values);
        keys.forEach(key => {
            const value = values[key]; // This is the declared type anyway, TypeScript is needing the cast
            if (isTReadOnlyProperty(value)) {
                dependencies.push(value);
            }
            let stringNumberMap = stringify;
            // If we are applying decimal places, "prepend" that map before the others
            if (options.decimalPlaces !== null && (typeof options.decimalPlaces === 'number' || options.decimalPlaces[key] !== null)) {
                // It won't be null (we checked above for hasDecimalPlaces), asserted below
                const decimalPlaces = (typeof options.decimalPlaces === 'number' || options.decimalPlaces === null)
                    ? options.decimalPlaces
                    : options.decimalPlaces[key];
                assert && assert(decimalPlaces !== null);
                stringNumberMap = (value) => stringify(typeof value === 'number' ? Utils.toFixed(value, decimalPlaces) : value);
            }
            // If we are applying a map, "prepend" that map before the others, so that if it returns a number, we can
            // apply decimal places to it.
            if (options.maps && options.maps.hasOwnProperty(key)) {
                maps[key] = value => stringNumberMap(options.maps[key](value));
            }
            else {
                maps[key] = stringNumberMap;
            }
        });
        // @ts-expect-error We can't get our dependencies to dynamically type here fully
        super(dependencies, (...propertyValues) => {
            const getValue = (value) => {
                const index = dependencies.indexOf(value);
                // If it's a Property, it will be in our dependencies, and we'll look up the value from our DerivedProperty
                return index >= 0 ? propertyValues[index] : value;
            };
            let result = `${getValue(patternProperty)}`; // String cast (it won't be a number, due to TypeScript)
            // Handle StringUtils.format compatibility, turning {0} => formatName[ 0 ], {1} => formatName[ 1 ], etc.
            options.formatNames.forEach((formatName, index) => {
                result = result.replace(new RegExp(`\\{${index}\\}`, 'g'), `{{${formatName}}}`);
            });
            // {string[]} parse out the set of placeholders
            const placeholders = result.match(/\{\{[^{}]+\}\}/g) || [];
            // replace each placeholder with its corresponding value
            for (let i = 0; i < placeholders.length; i++) {
                const placeholder = placeholders[i];
                // key is the portion of the placeholder between the curly braces
                const key = placeholder.replace('{{', '').replace('}}', '');
                if (keys.includes(key)) {
                    let value = getValue(values[key]);
                    if (maps[key]) {
                        value = maps[key](value);
                    }
                    result = result.replace(placeholder, `${value}`);
                }
            }
            return result;
        }, options);
    }
}
axon.register('PatternStringProperty', PatternStringProperty);
//# sourceMappingURL=PatternStringProperty.js.map