// Copyright 2021-2025, University of Colorado Boulder
/**
 * A Class that manages Simulation features that are enabled and disabled by user Preferences.
 *
 * @author Jesse Greenberg
 */
import '../../../globals.js';
import _ from 'lodash';
import '../../../chipper/js/browser/initialize-globals.js';
import BooleanProperty from '../../../axon/js/BooleanProperty.js';
import Multilink from '../../../axon/js/Multilink.js';
import merge from '../../../phet-core/js/merge.js';
import optionize from '../../../phet-core/js/optionize.js';
import voicingManager from '../../../scenery/js/accessibility/voicing/voicingManager.js';
import voicingUtteranceQueue from '../../../scenery/js/accessibility/voicing/voicingUtteranceQueue.js';
import colorProfileProperty from '../../../scenery/js/util/colorProfileProperty.js';
import soundManager from '../../../tambo/js/soundManager.js';
import PhetioObject from '../../../tandem/js/PhetioObject.js';
import Tandem from '../../../tandem/js/Tandem.js';
import BooleanIO from '../../../tandem/js/types/BooleanIO.js';
import IOType from '../../../tandem/js/types/IOType.js';
import responseCollector from '../../../utterance-queue/js/responseCollector.js';
import SpeechSynthesisAnnouncer from '../../../utterance-queue/js/SpeechSynthesisAnnouncer.js';
import audioManager from '../audioManager.js';
import localeProperty from '../i18n/localeProperty.js';
import { supportedRegionAndCultureValues } from '../i18n/regionAndCultureProperty.js';
import PreferencesStorage from './PreferencesStorage.js';
const AUDIO_MODEL_TANDEM = 'audioModel';
const VISUAL_MODEL_TANDEM = 'visualModel';
const INPUT_MODEL_TANDEM = 'inputModel';
class PreferencesModel extends PhetioObject {
    constructor(providedOptions = {}) {
        // initialize-globals uses package.json to determine defaults for features enabled by the sim and those defaults
        // can be overwritten by query parameter.  So phet.chipper.queryParameters contains an accurate representation of
        // which features are required.
        const phetFeaturesFromQueryParameters = phet.chipper.queryParameters;
        // Multiple optionize calls + spread in one initialization site so that TypeScript has the correct type for nested
        // options immediately, and we don't need multiple variables to achieve it.
        const options = {
            // Put the spread first so that nested options' defaults will correctly override
            // eslint-disable-next-line phet/no-object-spread-on-non-literals
            ...(optionize()({
                // phet-io
                tandem: Tandem.OPT_OUT, // Uninstrumented for now, but keep the file's instrumentation just in case, see https://github.com/phetsims/phet-io/issues/1913
                phetioType: PreferencesModel.PreferencesModelIO,
                phetioFeatured: true,
                phetioState: false,
                phetioReadOnly: true
            }, providedOptions)),
            simulationOptions: optionize()({
                tandemName: 'simulationModel',
                customPreferences: []
            }, providedOptions.simulationOptions),
            visualOptions: optionize()({
                tandemName: VISUAL_MODEL_TANDEM,
                supportsProjectorMode: false,
                supportsInteractiveHighlights: phetFeaturesFromQueryParameters.supportsInteractiveHighlights,
                customPreferences: []
            }, providedOptions.visualOptions),
            audioOptions: optionize()({
                tandemName: AUDIO_MODEL_TANDEM,
                supportsVoicing: phetFeaturesFromQueryParameters.supportsVoicing,
                supportsTier1Voicing: phetFeaturesFromQueryParameters.supportsTier1Voicing,
                supportsSound: phetFeaturesFromQueryParameters.supportsSound,
                supportsExtraSound: phetFeaturesFromQueryParameters.supportsExtraSound,
                customPreferences: []
            }, providedOptions.audioOptions),
            inputOptions: optionize()({
                tandemName: INPUT_MODEL_TANDEM,
                supportsGestureControl: phetFeaturesFromQueryParameters.supportsGestureControl,
                customPreferences: []
            }, providedOptions.inputOptions),
            localizationOptions: optionize()({
                tandemName: 'localizationModel',
                supportsDynamicLocale: !!localeProperty.availableRuntimeLocales && localeProperty.supportsDynamicLocale && phet.chipper.queryParameters.supportsDynamicLocale,
                customPreferences: [],
                includeLocalePanel: true
            }, providedOptions.localizationOptions)
        };
        super(options);
        this.simulationModel = options.simulationOptions;
        const visualTandem = options.tandem.createTandem(VISUAL_MODEL_TANDEM);
        this.visualModel = merge({
            interactiveHighlightsEnabledProperty: new BooleanProperty(phet.chipper.queryParameters.interactiveHighlightsInitiallyEnabled, {
                tandem: visualTandem.createTandem('interactiveHighlightsEnabledProperty'),
                phetioState: false
            }),
            colorProfileProperty: colorProfileProperty
        }, options.visualOptions);
        // For now, the Voicing feature is only available when we are running in the English locale, accessibility
        // strings are not made available for translation. When running with dynamic locales, the voicing feature
        // is supported if English is available, but will be disabled until English is selected.
        const voicingSupportedOnPlatform = SpeechSynthesisAnnouncer.isSpeechSynthesisSupported() &&
            (
            // Running with english locale OR an environment where locale switching is supported and
            // english is one of the available languages.
            phet.chipper.locale.startsWith('en') ||
                (phet.chipper.queryParameters.supportsDynamicLocale && _.some(localeProperty.availableRuntimeLocales, value => value.startsWith('en'))));
        // Audio can be disabled explicitly via query parameter
        const audioEnabled = audioManager.audioAllowed;
        const supportsVoicing = options.audioOptions.supportsVoicing && voicingSupportedOnPlatform && audioEnabled;
        const supportsTier1Voicing = options.audioOptions.supportsTier1Voicing && voicingSupportedOnPlatform && audioEnabled;
        const supportsAnyVoicing = supportsVoicing || supportsTier1Voicing;
        this.audioModel = {
            supportsVoicing: supportsVoicing,
            supportsTier1Voicing: supportsTier1Voicing,
            supportsAnyVoicing: supportsAnyVoicing,
            supportsSound: options.audioOptions.supportsSound && audioEnabled,
            supportsExtraSound: options.audioOptions.supportsExtraSound && audioEnabled,
            audioEnabledProperty: audioManager.audioEnabledProperty,
            soundEnabledProperty: soundManager.enabledProperty,
            extraSoundEnabledProperty: soundManager.extraSoundEnabledProperty,
            voicingEnabledProperty: voicingManager.enabledProperty,
            voicingMainWindowVoicingEnabledProperty: voicingManager.mainWindowVoicingEnabledProperty,
            voicingObjectResponsesEnabledProperty: responseCollector.objectResponsesEnabledProperty,
            voicingContextResponsesEnabledProperty: responseCollector.contextResponsesEnabledProperty,
            voicingHintResponsesEnabledProperty: responseCollector.hintResponsesEnabledProperty,
            voicePitchProperty: voicingManager.voicePitchProperty,
            voiceRateProperty: voicingManager.voiceRateProperty,
            voiceProperty: voicingManager.voiceProperty,
            // The toolbar is enabled by default, but can be initially disabled with a query parameter.
            toolbarEnabledProperty: new BooleanProperty(!phet.chipper.queryParameters.voicingRemoveToolbar, {
                tandem: options.tandem.createTandem(AUDIO_MODEL_TANDEM).createTandem('toolbarEnabledProperty'),
                phetioState: false
            }),
            customPreferences: options.audioOptions.customPreferences,
            tandemName: options.audioOptions.tandemName
        };
        const inputTandem = options.tandem.createTandem(INPUT_MODEL_TANDEM);
        this.inputModel = merge({
            gestureControlsEnabledProperty: new BooleanProperty(false, {
                tandem: inputTandem.createTandem('gestureControlsEnabledProperty'),
                phetioState: false
            })
        }, options.inputOptions);
        this.localizationModel = merge({
            localeProperty: localeProperty
        }, options.localizationOptions);
        if (this.audioModel.supportsExtraSound) {
            ;
        }
        // Only supportsVoicing OR supportsTier1Voicing can be true at one time.
        ;
        this.addPhetioLinkedElementsForModel(options.tandem, this.simulationModel);
        this.addPhetioLinkedElementsForModel(options.tandem, this.visualModel, [
            { property: this.visualModel.colorProfileProperty }
        ]);
        this.addPhetioLinkedElementsForModel(options.tandem, this.audioModel, [
            { property: this.audioModel.audioEnabledProperty, tandemName: 'audioEnabledProperty' },
            { property: this.audioModel.soundEnabledProperty, tandemName: 'soundEnabledProperty' },
            { property: this.audioModel.extraSoundEnabledProperty, tandemName: 'extraSoundEnabledProperty' },
            { property: this.audioModel.voicingEnabledProperty, tandemName: 'voicingEnabledProperty' },
            { property: this.audioModel.voicingMainWindowVoicingEnabledProperty, tandemName: 'voicingMainWindowVoicingEnabledProperty' },
            { property: this.audioModel.voicingObjectResponsesEnabledProperty, tandemName: 'voicingObjectResponsesEnabledProperty' },
            { property: this.audioModel.voicingContextResponsesEnabledProperty, tandemName: 'voicingContextResponsesEnabledProperty' },
            { property: this.audioModel.voicingHintResponsesEnabledProperty, tandemName: 'voicingHintResponsesEnabledProperty' },
            { property: this.audioModel.voicePitchProperty, tandemName: 'voicePitchProperty' },
            { property: this.audioModel.voiceRateProperty, tandemName: 'voiceRateProperty' }
        ]);
        this.addPhetioLinkedElementsForModel(options.tandem, this.inputModel);
        this.addPhetioLinkedElementsForModel(options.tandem, this.localizationModel, [
            { property: this.localizationModel.localeProperty, tandemName: 'localeProperty' }
        ]);
        // Since voicingManager in Scenery can not use initialize-globals, set the initial value for whether Voicing is
        // enabled here in the PreferencesModel.
        if (supportsAnyVoicing) {
            voicingManager.enabledProperty.value = phet.chipper.queryParameters.voicingInitiallyEnabled;
            // Voicing is only available in the 'en' locale currently. If the locale is changed away from English, Voicing is
            // disabled. The next time Voicing returns to 'en', Voicing will be enabled again.
            let voicingDisabledFromLocale = false;
            localeProperty.link(locale => {
                const englishLocale = voicingManager.voicingSupportedForLocale(locale);
                if (voicingManager.enabledProperty.value) {
                    voicingManager.enabledProperty.value = englishLocale;
                    voicingDisabledFromLocale = true;
                }
                else if (voicingDisabledFromLocale && englishLocale) {
                    voicingManager.enabledProperty.value = true;
                    voicingDisabledFromLocale = false;
                }
            });
            // The default utteranceQueue will be used for voicing of simulation components, and it is enabled when the
            // voicingManager is fully enabled (voicingManager is enabled and the voicing is enabled for the "main window"
            // sim screens)
            voicingManager.enabledProperty.link(enabled => {
                voicingUtteranceQueue.enabled = enabled;
                !enabled && voicingUtteranceQueue.clear();
            });
            // If initially enabled, apply a prioritized default voice.
            if (phet.chipper.queryParameters.voicingInitiallyEnabled) {
                // Set the first voice according to PhET's preferred english voices
                const voicesMultilink = Multilink.multilink([voicingManager.voicesProperty, voicingManager.isInitializedProperty], (voices, initialized) => {
                    if (initialized && voices.length > 0) {
                        voicingManager.voiceProperty.value = voicingManager.getEnglishPrioritizedVoices()[0];
                        Multilink.unmultilink(voicesMultilink);
                    }
                });
            }
            // Feature specific query parameters set the initial state of the voicing features. These can (and should) be
            // overwritten by PreferencesStorage.
            responseCollector.objectResponsesEnabledProperty.value = !!phet.chipper.queryParameters.voicingAddObjectResponses;
            responseCollector.contextResponsesEnabledProperty.value = !!phet.chipper.queryParameters.voicingAddContextResponses;
            responseCollector.hintResponsesEnabledProperty.value = !!phet.chipper.queryParameters.voicingAddHintResponses;
        }
        if (phet.chipper.queryParameters.printVoicingResponses) {
            voicingManager.startSpeakingEmitter.addListener(text => console.log(text));
        }
        this.registerPreferencesStorage();
    }
    /**
     * Set up preferencesStorage for supported PreferencesProperties. Don't include all-sound and all-audio controls
     * because that feel too global to automatically take the last value.
     */
    registerPreferencesStorage() {
        if (this.visualModel.supportsInteractiveHighlights) {
            PreferencesStorage.register(this.visualModel.interactiveHighlightsEnabledProperty, 'interactiveHighlightsEnabledProperty');
        }
        if (this.audioModel.supportsAnyVoicing) {
            // Register these to be stored when PreferencesStorage is enabled.
            PreferencesStorage.register(this.audioModel.voicingObjectResponsesEnabledProperty, 'objectResponsesEnabledProperty');
            PreferencesStorage.register(this.audioModel.voicingContextResponsesEnabledProperty, 'contextResponsesEnabledProperty');
            PreferencesStorage.register(this.audioModel.voicingHintResponsesEnabledProperty, 'hintResponsesEnabledProperty');
            PreferencesStorage.register(this.audioModel.voiceRateProperty, 'voiceRateProperty');
            PreferencesStorage.register(this.audioModel.voicePitchProperty, 'voicePitchProperty');
        }
        if (this.audioModel.supportsExtraSound) {
            PreferencesStorage.register(this.audioModel.extraSoundEnabledProperty, 'extraSoundEnabledProperty');
        }
        if (this.inputModel.supportsGestureControl) {
            PreferencesStorage.register(this.inputModel.gestureControlsEnabledProperty, 'gestureControlsEnabledProperty');
        }
    }
    addPhetioLinkedElementsForModel(parentTandem, featureModel, additionalProperties = []) {
        const tandem = parentTandem.createTandem(featureModel.tandemName);
        const propertiesToLink = additionalProperties;
        for (let j = 0; j < propertiesToLink.length; j++) {
            const modelPropertyObject = propertiesToLink[j];
            const tandemName = modelPropertyObject.tandemName || modelPropertyObject.property.tandem.name;
            this.addLinkedElement(modelPropertyObject.property, { tandem: tandem.createTandem(tandemName) });
        }
    }
    preferenceModelHasCustom(preferenceModel) {
        return preferenceModel.customPreferences.length > 0;
    }
    /**
     * Returns true if the SimulationModel supports any preferences that can be changed.
     */
    supportsSimulationPreferences() {
        return this.preferenceModelHasCustom(this.simulationModel);
    }
    /**
     * Returns true if the VisualModel has any preferences that can be changed.
     */
    supportsVisualPreferences() {
        return this.visualModel.supportsInteractiveHighlights ||
            this.visualModel.supportsProjectorMode ||
            this.preferenceModelHasCustom(this.visualModel);
    }
    /**
     * Returns true if the AudioModel has any preferences that can be changed.
     */
    supportsAudioPreferences() {
        // If audio is disabled, no audio preferences are supported. Note that audioModel.supports*
        // fields are false when audio is disabled. But preferenceModelHasCustom may still return true.
        if (!audioManager.audioAllowed) {
            return false;
        }
        return this.audioModel.supportsSound ||
            this.audioModel.supportsExtraSound ||
            this.audioModel.supportsAnyVoicing ||
            this.preferenceModelHasCustom(this.audioModel);
    }
    /**
     * Returns true if the InputModel has any preferences that can be changed.
     */
    supportsInputPreferences() {
        return this.inputModel.supportsGestureControl || this.preferenceModelHasCustom(this.inputModel);
    }
    /**
     * Returns true if the LocalizationModel has any preferences that can be changed.
     */
    supportsLocalizationPreferences() {
        return this.localizationModel.supportsDynamicLocale ||
            supportedRegionAndCultureValues.length > 1 ||
            this.preferenceModelHasCustom(this.localizationModel);
    }
    /**
     * Returns true if this model supports any controllable preferences for the dialog. Returns false when the dialog
     * would have nothing to display.
     */
    shouldShowDialog() {
        return this.supportsSimulationPreferences() || this.supportsVisualPreferences() ||
            this.supportsInputPreferences() || this.supportsLocalizationPreferences() ||
            this.supportsAudioPreferences();
    }
}
PreferencesModel.PreferencesModelIO = new IOType('PreferencesModelIO', {
    valueType: PreferencesModel,
    toStateObject: (preferencesModel) => {
        return {
            supportsProjectorMode: preferencesModel.visualModel.supportsProjectorMode,
            supportsInteractiveHighlights: preferencesModel.visualModel.supportsInteractiveHighlights,
            supportsVoicing: preferencesModel.audioModel.supportsVoicing,
            supportsTier1Voicing: preferencesModel.audioModel.supportsTier1Voicing,
            supportsSound: preferencesModel.audioModel.supportsSound,
            supportsExtraSound: preferencesModel.audioModel.supportsExtraSound,
            supportsGestureControl: preferencesModel.inputModel.supportsGestureControl,
            supportsDynamicLocale: preferencesModel.localizationModel.supportsDynamicLocale,
            // Method-based
            supportsAudioPreferences: preferencesModel.supportsAudioPreferences(),
            supportsInputPreferences: preferencesModel.supportsInputPreferences(),
            supportsLocalizationPreferences: preferencesModel.supportsLocalizationPreferences(),
            supportsSimulationPreferences: preferencesModel.supportsSimulationPreferences(),
            supportsVisualPreferences: preferencesModel.supportsVisualPreferences()
        };
    },
    stateSchema: {
        supportsProjectorMode: BooleanIO,
        supportsInteractiveHighlights: BooleanIO,
        supportsVoicing: BooleanIO,
        supportsTier1Voicing: BooleanIO,
        supportsSound: BooleanIO,
        supportsExtraSound: BooleanIO,
        supportsGestureControl: BooleanIO,
        supportsDynamicLocale: BooleanIO,
        // Method-based
        supportsAudioPreferences: BooleanIO,
        supportsInputPreferences: BooleanIO,
        supportsLocalizationPreferences: BooleanIO,
        supportsSimulationPreferences: BooleanIO,
        supportsVisualPreferences: BooleanIO
    }
});
export default PreferencesModel;
//# sourceMappingURL=PreferencesModel.js.map