import _ from 'lodash';
import '../../chipper/js/browser/initialize-globals.js';
import '../../assert/js/assert.js';
import '../../globals.js';
// Copyright 2015-2024, University of Colorado Boulder
/**
 * Displays a Property of type {number} in a background rectangle.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
import DerivedProperty from '../../axon/js/DerivedProperty.js';
import TinyProperty from '../../axon/js/TinyProperty.js';
import Utils from '../../dot/js/Utils.js';
import Vector2 from '../../dot/js/Vector2.js';
import optionize, { combineOptions } from '../../phet-core/js/optionize.js';
import StringUtils from '../../phetcommon/js/util/StringUtils.js';
import { ManualConstraint, Node, Rectangle, RichText, Text } from '../../scenery/js/imports.js';
import SunConstants from '../../sun/js/SunConstants.js';
import Tandem from '../../tandem/js/Tandem.js';
import IOType from '../../tandem/js/types/IOType.js';
import StringIO from '../../tandem/js/types/StringIO.js';
import MathSymbols from './MathSymbols.js';
import PhetFont from './PhetFont.js';
import sceneryPhet from './sceneryPhet.js';
// constants
const DEFAULT_FONT = new PhetFont(20);
// valid values for options.align and options.noValueAlign
const ALIGN_VALUES = ['center', 'left', 'right'];
const DEFAULT_DECIMAL_PLACES = 0;
class NumberDisplay extends Node {
    /**
     * @param numberProperty
     * @param displayRange - this range, with options.decimals or numberFormatter applied, is used to determine
     *                     - the display width. It is unrelated to the range of numberProperty.
     * @param providedOptions
     */
    constructor(numberProperty, displayRange, providedOptions) {
        const options = optionize()({
            align: 'right',
            valuePattern: SunConstants.VALUE_NAMED_PLACEHOLDER,
            decimalPlaces: DEFAULT_DECIMAL_PLACES,
            numberFormatter: null,
            numberFormatterDependencies: [],
            useRichText: false,
            useFullHeight: false,
            textOptions: {
                font: DEFAULT_FONT,
                fill: 'black',
                maxWidth: null, // {number|null} if null, then it will be computed based on displayRange
                phetioReadOnly: true
            },
            xMargin: 8,
            yMargin: 2,
            cornerRadius: 0,
            backgroundFill: 'white',
            backgroundStroke: 'lightGray',
            backgroundLineWidth: 1,
            backgroundLineDash: [],
            minBackgroundWidth: 0,
            noValueString: MathSymbols.NO_VALUE,
            noValueAlign: null,
            noValuePattern: null,
            // phet-io
            tandem: Tandem.OPT_OUT,
            phetioType: NumberDisplay.NumberDisplayIO
        }, providedOptions);
        // valuePattern|decimalPlaces is mutually exclusive with numberFormatter
        if (assert) {
            const numberFormatterProvided = !!options.numberFormatter;
            const decimalPlacesProvided = options.decimalPlaces !== DEFAULT_DECIMAL_PLACES;
            const valuePatternProvided = options.valuePattern !== SunConstants.VALUE_NAMED_PLACEHOLDER;
            const decimalOrValueProvided = decimalPlacesProvided || valuePatternProvided;
            if (numberFormatterProvided || decimalOrValueProvided) {
                assert && assert(numberFormatterProvided !== decimalOrValueProvided, 'options.numberFormatter is mutually exclusive with options.valuePattern and options.decimalPlaces');
            }
        }
        const numberFormatterProperty = new TinyProperty(options.numberFormatter ? options.numberFormatter : (value) => {
            if (options.decimalPlaces === null) {
                return `${value}`;
            }
            else {
                return Utils.toFixed(value, options.decimalPlaces);
            }
        });
        assert && assert(!options.hasOwnProperty('unitsNode'), 'unitsNode is not a supported option');
        // Set default alignments and validate
        assert && assert(_.includes(ALIGN_VALUES, options.align), `invalid align: ${options.align}`);
        if (!options.noValueAlign) {
            options.noValueAlign = options.align;
        }
        assert && assert(_.includes(ALIGN_VALUES, options.noValueAlign), `invalid noValueAlign: ${options.noValueAlign}`);
        assert && assert(options.textOptions, 'did you accidentally set textOptions to null?');
        // Support numbered (old-style) placeholder by replacing it with the corresponding named placeholder.
        // See https://github.com/phetsims/scenery-phet/issues/446
        const replaceValuePatternValue = (valuePattern) => {
            if (valuePattern.includes(SunConstants.VALUE_NUMBERED_PLACEHOLDER)) {
                return StringUtils.format(valuePattern, SunConstants.VALUE_NAMED_PLACEHOLDER);
            }
            else {
                return valuePattern;
            }
        };
        const valuePatternProperty = (typeof options.valuePattern === 'string')
            ? new TinyProperty(replaceValuePatternValue(options.valuePattern))
            : new DerivedProperty([options.valuePattern], replaceValuePatternValue);
        assert && assert(!!phet?.chipper?.queryParameters?.stringTest ||
            valuePatternProperty.value.includes(SunConstants.VALUE_NAMED_PLACEHOLDER), `missing value placeholder in options.valuePattern: ${valuePatternProperty.value}`);
        // Set default and validate
        if (!options.noValuePattern) {
            // So we don't have duplicated Properties in our DerivedProperty (it's not supported by that)
            options.noValuePattern = new DerivedProperty([valuePatternProperty], x => x);
        }
        const noValuePatternProperty = typeof options.noValuePattern === 'string' ? new TinyProperty(options.noValuePattern) : options.noValuePattern;
        assert && assert(!!phet?.chipper?.queryParameters?.stringTest ||
            noValuePatternProperty.value.includes(SunConstants.VALUE_NAMED_PLACEHOLDER), `missing value placeholder in options.noValuePattern: ${noValuePatternProperty.value}`);
        // determine the widest value
        const minStringProperty = DerivedProperty.deriveAny([numberFormatterProperty, ...options.numberFormatterDependencies], () => {
            return valueToString(displayRange.min, options.noValueString, numberFormatterProperty.value);
        });
        const maxStringProperty = DerivedProperty.deriveAny([numberFormatterProperty, ...options.numberFormatterDependencies], () => {
            return valueToString(displayRange.max, options.noValueString, numberFormatterProperty.value);
        });
        const longestStringProperty = new DerivedProperty([
            valuePatternProperty,
            minStringProperty,
            maxStringProperty
        ], (valuePattern, minString, maxString) => {
            return StringUtils.fillIn(valuePattern, {
                value: ((minString.length > maxString.length) ? minString : maxString)
            });
        });
        // value
        const ValueTextConstructor = options.useRichText ? RichText : Text;
        const valueTextTandem = options.textOptions.tandem || options.tandem.createTandem('valueText');
        const valueStringProperty = DerivedProperty.deriveAny([numberProperty, noValuePatternProperty, valuePatternProperty, numberFormatterProperty, ...options.numberFormatterDependencies], () => {
            const value = numberProperty.value;
            const noValuePattern = noValuePatternProperty.value;
            const valuePatternValue = valuePatternProperty.value;
            const numberFormatter = numberFormatterProperty.value;
            const valuePattern = (value === null && noValuePattern) ? noValuePattern : valuePatternValue;
            const stringValue = valueToString(value, options.noValueString, numberFormatter);
            return StringUtils.fillIn(valuePattern, {
                value: stringValue
            });
        }, {
            // These options were copied here when we moved from DerivedStringProperty to DerivedProperty.
            phetioFeatured: true,
            phetioValueType: StringIO,
            tandemNameSuffix: 'StringProperty',
            tandem: valueTextTandem.createTandem(Text.STRING_PROPERTY_TANDEM_NAME)
        });
        const valueTextOptions = combineOptions({
            tandem: valueTextTandem
        }, options.textOptions, {
            maxWidth: null // we are handling maxWidth manually, so we don't want to provide it initially.
        });
        const valueText = new ValueTextConstructor(valueStringProperty, valueTextOptions);
        const originalTextHeight = valueText.height;
        // background rectangle
        const backgroundNode = new Rectangle({
            cornerRadius: options.cornerRadius,
            fill: options.backgroundFill,
            stroke: options.backgroundStroke,
            lineWidth: options.backgroundLineWidth,
            lineDash: options.backgroundLineDash
        });
        // Manually set maxWidth later, adjusting it to the width of the longest string if it's null
        longestStringProperty.link(longestString => {
            const demoText = new ValueTextConstructor(longestString, _.omit(valueTextOptions, 'tandem'));
            valueText.maxWidth = (options.textOptions.maxWidth !== null) ? options.textOptions.maxWidth :
                (demoText.width !== 0) ? demoText.width : null;
            demoText.maxWidth = valueText.maxWidth;
            backgroundNode.rectWidth = Math.max(options.minBackgroundWidth, demoText.width + 2 * options.xMargin);
            backgroundNode.rectHeight = (options.useFullHeight ? originalTextHeight : demoText.height) + 2 * options.yMargin;
        });
        // Avoid infinite loops like https://github.com/phetsims/axon/issues/447 by applying the maxWidth to a different Node
        // than the one that is used for layout.
        const valueTextContainer = new Node({
            children: [valueText]
        });
        options.children = [backgroundNode, valueTextContainer];
        super();
        this.numberFormatterProperty = numberFormatterProperty;
        this.valueText = valueText;
        this.backgroundNode = backgroundNode;
        this.valueStringProperty = valueStringProperty;
        // Align the value in the background.
        ManualConstraint.create(this, [valueTextContainer, backgroundNode], (valueTextContainerProxy, backgroundNodeProxy) => {
            // Alignment depends on whether we have a non-null value.
            const align = (numberProperty.value === null) ? options.noValueAlign : options.align;
            // vertical alignment
            const centerY = backgroundNodeProxy.centerY;
            // horizontal alignment
            if (align === 'center') {
                valueTextContainerProxy.center = new Vector2(backgroundNodeProxy.centerX, centerY);
            }
            else if (align === 'left') {
                valueTextContainerProxy.leftCenter = new Vector2(backgroundNodeProxy.left + options.xMargin, centerY);
            }
            else { // right
                valueTextContainerProxy.rightCenter = new Vector2(backgroundNodeProxy.right - options.xMargin, centerY);
            }
        });
        this.mutate(options);
        this.disposeNumberDisplay = () => {
            valueStringProperty.dispose();
            valuePatternProperty.dispose();
        };
    }
    dispose() {
        this.disposeNumberDisplay();
        super.dispose();
    }
    /**
     * Sets the number text font.
     */
    setNumberFont(font) {
        this.valueText.font = font;
    }
    set numberFont(value) { this.setNumberFont(value); }
    /**
     * Sets the number text fill.
     */
    setNumberFill(fill) {
        this.valueText.fill = fill;
    }
    set numberFill(value) { this.setNumberFill(value); }
    /**
     * Sets the background fill.
     */
    setBackgroundFill(fill) {
        this.backgroundNode.fill = fill;
    }
    set backgroundFill(value) { this.setBackgroundFill(value); }
    get backgroundFill() {
        return this.getBackgroundFill();
    }
    /**
     * Gets the background fill.
     */
    getBackgroundFill() {
        return this.backgroundNode.fill;
    }
    /**
     * Sets the background stroke.
     */
    setBackgroundStroke(stroke) {
        this.backgroundNode.stroke = stroke;
    }
    set backgroundStroke(value) { this.setBackgroundStroke(value); }
    /**
     * Get the width of the background.
     */
    getBackgroundWidth() {
        return this.backgroundNode.getRectWidth();
    }
    /**
     * Set the width of the background node.
     */
    setBackgroundWidth(width) {
        this.backgroundNode.setRectWidth(width);
    }
    get backgroundWidth() { return this.getBackgroundWidth(); }
    set backgroundWidth(width) { this.setBackgroundWidth(width); }
}
NumberDisplay.NumberDisplayIO = new IOType('NumberDisplayIO', {
    valueType: NumberDisplay,
    supertype: Node.NodeIO,
    documentation: 'A numeric readout with a background'
});
export default NumberDisplay;
sceneryPhet.register('NumberDisplay', NumberDisplay);
/**
 * Converts a numeric value to a string.
 * @param value
 * @param decimalPlaces - if null, use the full value
 * @param noValueString
 * @param numberFormatter - if provided, function that converts {number} => {string}
 */
const valueToString = (value, noValueString, numberFormatter) => {
    let stringValue = noValueString;
    if (value !== null) {
        stringValue = numberFormatter(value);
    }
    return stringValue;
};
//# sourceMappingURL=NumberDisplay.js.map