import '../../../assert/js/assert.js';
import '../../../tandem/js/PhetioIDUtils.js';
import '../../../globals.js';
// Copyright 2020-2024, University of Colorado Boulder
/**
 * ButtonNode is the base class for the sun button hierarchy.
 *
 * @author John Blanco (PhET Interactive Simulations)
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Michael Kauzmann (PhET Interactive Simulations)
 */
import DerivedProperty from '../../../axon/js/DerivedProperty.js';
import Multilink from '../../../axon/js/Multilink.js';
import Bounds2 from '../../../dot/js/Bounds2.js';
import Dimension2 from '../../../dot/js/Dimension2.js';
import optionize, { combineOptions } from '../../../phet-core/js/optionize.js';
import { AlignBox, assertNoAdditionalChildren, Brightness, Contrast, Grayscale, Node, PaintColorProperty, SceneryConstants, Sizable, Voicing } from '../../../scenery/js/imports.js';
import ColorConstants from '../ColorConstants.js';
import sun from '../sun.js';
import ButtonInteractionState from './ButtonInteractionState.js';
import TinyProperty from '../../../axon/js/TinyProperty.js';
// constants
const CONTRAST_FILTER = new Contrast(0.7);
const BRIGHTNESS_FILTER = new Brightness(1.2);
export default class ButtonNode extends Sizable(Voicing(Node)) {
    /**
     * @param buttonModel
     * @param buttonBackground - the background of the button (like a circle or rectangle).
     * @param interactionStateProperty - a Property that is used to drive the visual appearance of the button
     * @param providedOptions - this type does not mutate its options, but relies on the subtype to
     */
    constructor(buttonModel, buttonBackground, interactionStateProperty, providedOptions) {
        const options = optionize()({
            content: null,
            xMargin: 10,
            yMargin: 5,
            xAlign: 'center',
            yAlign: 'center',
            xContentOffset: 0,
            yContentOffset: 0,
            baseColor: ColorConstants.LIGHT_BLUE,
            cursor: 'pointer',
            buttonAppearanceStrategy: ButtonNode.FlatAppearanceStrategy,
            buttonAppearanceStrategyOptions: {},
            contentAppearanceStrategy: null,
            contentAppearanceStrategyOptions: {},
            enabledAppearanceStrategy: (enabled, button, background, content) => {
                background.filters = enabled ? [] : [CONTRAST_FILTER, BRIGHTNESS_FILTER];
                if (content) {
                    content.filters = enabled ? [] : [Grayscale.FULL];
                    content.opacity = enabled ? 1 : SceneryConstants.DISABLED_OPACITY;
                }
            },
            disabledColor: ColorConstants.LIGHT_GRAY,
            // pdom
            tagName: 'button',
            // phet-io
            tandemNameSuffix: 'Button',
            visiblePropertyOptions: { phetioFeatured: true },
            phetioEnabledPropertyInstrumented: true // opt into default PhET-iO instrumented enabledProperty
        }, providedOptions);
        options.listenerOptions = combineOptions({
            tandem: options.tandem?.createTandem('pressListener')
        }, options.listenerOptions);
        assert && options.enabledProperty && assert(options.enabledProperty === buttonModel.enabledProperty, 'if options.enabledProperty is provided, it must === buttonModel.enabledProperty');
        options.enabledProperty = buttonModel.enabledProperty;
        super();
        this.contentContainer = null; // (sun-only)
        this.layoutSizeProperty = new TinyProperty(new Dimension2(0, 0));
        this.content = options.content;
        this.buttonModel = buttonModel;
        this._settableBaseColorProperty = new PaintColorProperty(options.baseColor);
        this._disabledColorProperty = new PaintColorProperty(options.disabledColor);
        this.baseColorProperty = new DerivedProperty([
            this._settableBaseColorProperty,
            this.enabledProperty,
            this._disabledColorProperty
        ], (color, enabled, disabledColor) => {
            return enabled ? color : disabledColor;
        });
        this._pressListener = buttonModel.createPressListener(options.listenerOptions);
        this.addInputListener(this._pressListener);
        assert && assert(buttonBackground.fill === null, 'ButtonNode controls the fill for the buttonBackground');
        buttonBackground.fill = this.baseColorProperty;
        this.addChild(buttonBackground);
        // Hook up the strategy that will control the button's appearance.
        const buttonAppearanceStrategy = new options.buttonAppearanceStrategy(buttonBackground, interactionStateProperty, this.baseColorProperty, options.buttonAppearanceStrategyOptions);
        // Optionally hook up the strategy that will control the content's appearance.
        let contentAppearanceStrategy;
        if (options.contentAppearanceStrategy && options.content) {
            contentAppearanceStrategy = new options.contentAppearanceStrategy(options.content, interactionStateProperty, options.contentAppearanceStrategyOptions);
        }
        // Get our maxLineWidth from the appearance strategy, as it's needed for layout (and in subtypes)
        this.maxLineWidth = buttonAppearanceStrategy.maxLineWidth;
        let alignBox = null;
        let updateAlignBounds = null;
        if (options.content) {
            // Container here that can get scaled/positioned/pickable-modified, without affecting the provided content.
            this.contentContainer = new Node({
                children: [
                    options.content
                ],
                // For performance, in case content is a complicated icon or shape.
                // See https://github.com/phetsims/sun/issues/654#issuecomment-718944669
                pickable: false
            });
            // Align content in the button rectangle. Must be disposed since it adds listener to content bounds.
            alignBox = new AlignBox(this.contentContainer, {
                xAlign: options.xAlign,
                yAlign: options.yAlign,
                // Apply offsets via margins, so that bounds of the AlignBox doesn't unnecessarily extend past the
                // buttonBackground. See https://github.com/phetsims/sun/issues/649
                leftMargin: options.xMargin + options.xContentOffset,
                rightMargin: options.xMargin - options.xContentOffset,
                topMargin: options.yMargin + options.yContentOffset,
                bottomMargin: options.yMargin - options.yContentOffset
            });
            // Dynamically adjust alignBounds.
            updateAlignBounds = Multilink.multilink([buttonBackground.boundsProperty, this.layoutSizeProperty], (backgroundBounds, size) => {
                if (size.width > 0 && size.height > 0) {
                    alignBox.alignBounds = Bounds2.point(backgroundBounds.center).dilatedXY(size.width / 2, size.height / 2);
                }
            });
            this.addChild(alignBox);
        }
        this.mutate(options);
        // No need to dispose because enabledProperty is disposed in Node
        this.enabledProperty.link(enabled => options.enabledAppearanceStrategy(enabled, this, buttonBackground, alignBox));
        // Decorating with additional content is an anti-pattern, see https://github.com/phetsims/sun/issues/860
        assert && assertNoAdditionalChildren(this);
        this.disposeButtonNode = () => {
            alignBox && alignBox.dispose();
            updateAlignBounds && updateAlignBounds.dispose();
            buttonAppearanceStrategy.dispose && buttonAppearanceStrategy.dispose();
            contentAppearanceStrategy && contentAppearanceStrategy.dispose && contentAppearanceStrategy.dispose();
            this._pressListener.dispose();
            this.baseColorProperty.dispose();
            this._settableBaseColorProperty.dispose();
            this._disabledColorProperty.dispose();
        };
    }
    dispose() {
        this.disposeButtonNode();
        super.dispose();
    }
    /**
     * Sets the base color, which is the main background fill color used for the button.
     */
    setBaseColor(baseColor) { this._settableBaseColorProperty.paint = baseColor; }
    set baseColor(baseColor) { this.setBaseColor(baseColor); }
    get baseColor() { return this.getBaseColor(); }
    /**
     * Gets the base color for this button.
     */
    getBaseColor() { return this._settableBaseColorProperty.paint; }
    /**
     * Manually click the button, as it would be clicked in response to alternative input. Recommended only for
     * accessibility usages. For the most part, PDOM button functionality should be managed by PressListener, this should
     * rarely be used.
     */
    pdomClick() {
        this._pressListener.click(null);
    }
}
/**
 * FlatAppearanceStrategy is a value for ButtonNode options.buttonAppearanceStrategy. It makes a
 * button look flat, i.e. no shading or highlighting, with color changes on mouseover, press, etc.
 */
export class FlatAppearanceStrategy {
    /**
     * @param buttonBackground - the Node for the button's background, sans content
     * @param interactionStateProperty - interaction state, used to trigger updates
     * @param baseColorProperty - base color from which other colors are derived
     * @param [providedOptions]
     */
    constructor(buttonBackground, interactionStateProperty, baseColorProperty, providedOptions) {
        // dynamic colors
        const baseBrighter4Property = new PaintColorProperty(baseColorProperty, { luminanceFactor: 0.4 });
        const baseDarker4Property = new PaintColorProperty(baseColorProperty, { luminanceFactor: -0.4 });
        // various fills that are used to alter the button's appearance
        const upFillProperty = baseColorProperty;
        const overFillProperty = baseBrighter4Property;
        const downFillProperty = baseDarker4Property;
        const options = combineOptions({
            stroke: baseDarker4Property
        }, providedOptions);
        const lineWidth = typeof options.lineWidth === 'number' ? options.lineWidth : 1;
        // If the stroke wasn't provided, set a default.
        buttonBackground.stroke = options.stroke || baseDarker4Property;
        buttonBackground.lineWidth = lineWidth;
        this.maxLineWidth = buttonBackground.hasStroke() ? lineWidth : 0;
        // Cache colors
        buttonBackground.cachedPaints = [upFillProperty, overFillProperty, downFillProperty];
        // Change colors to match interactionState
        function interactionStateListener(interactionState) {
            switch (interactionState) {
                case ButtonInteractionState.IDLE:
                    buttonBackground.fill = upFillProperty;
                    break;
                case ButtonInteractionState.OVER:
                    buttonBackground.fill = overFillProperty;
                    break;
                case ButtonInteractionState.PRESSED:
                    buttonBackground.fill = downFillProperty;
                    break;
                default:
                    throw new Error(`unsupported interactionState: ${interactionState}`);
            }
        }
        // Do the initial update explicitly, then lazy link to the properties.  This keeps the number of initial updates to
        // a minimum and allows us to update some optimization flags the first time the base color is actually changed.
        interactionStateProperty.link(interactionStateListener);
        this.disposeFlatAppearanceStrategy = () => {
            if (interactionStateProperty.hasListener(interactionStateListener)) {
                interactionStateProperty.unlink(interactionStateListener);
            }
            baseBrighter4Property.dispose();
            baseDarker4Property.dispose();
        };
    }
    dispose() {
        this.disposeFlatAppearanceStrategy();
    }
}
ButtonNode.FlatAppearanceStrategy = FlatAppearanceStrategy;
sun.register('ButtonNode', ButtonNode);
//# sourceMappingURL=ButtonNode.js.map