// Copyright Epic Games, Inc. All Rights Reserved.

import { Logger } from '../Logger/Logger';
import { SettingFlag } from './SettingFlag';
import { SettingNumber } from './SettingNumber';
import { SettingText } from './SettingText';
import { SettingOption } from './SettingOption';
import { EventEmitter, SettingsChangedEvent } from '../Util/EventEmitter';
import { SettingBase } from './SettingBase';

/**
 * A collection of flags that can be toggled and are core to all Pixel Streaming experiences.
 * These are used in the `Config.Flags` map.
 */
export class Flags {
    static AutoConnect = 'AutoConnect' as const;
    static AutoPlayVideo = 'AutoPlayVideo' as const;
    static AFKDetection = 'TimeoutIfIdle' as const;
    static BrowserSendOffer = 'OfferToReceive' as const;
    static HoveringMouseMode = 'HoveringMouse' as const;
    static ForceMonoAudio = 'ForceMonoAudio' as const;
    static ForceTURN = 'ForceTURN' as const;
    static FakeMouseWithTouches = 'FakeMouseWithTouches' as const;
    static IsQualityController = 'ControlsQuality' as const;
    static MatchViewportResolution = 'MatchViewportRes' as const;
    static StartVideoMuted = 'StartVideoMuted' as const;
    static SuppressBrowserKeys = 'SuppressBrowserKeys' as const;
    static UseMic = 'UseMic' as const;
    static KeyboardInput = 'KeyboardInput' as const;
    static MouseInput = 'MouseInput' as const;
    static TouchInput = 'TouchInput' as const;
    static GamepadInput = 'GamepadInput' as const;
    static XRControllerInput = 'XRControllerInput' as const;
    static WaitForStreamer = 'WaitForStreamer' as const;
    static HideUI = 'HideUI' as const;
}

export type FlagsKeys = Exclude<keyof typeof Flags, 'prototype'>;
export type FlagsIds = typeof Flags[FlagsKeys];

const isFlagId = (id: string): id is FlagsIds =>
    Object.getOwnPropertyNames(Flags).some(
        (name: FlagsKeys) => Flags[name] === id
    );

/**
 * A collection of numeric parameters that are core to all Pixel Streaming experiences.
 *
 */
export class NumericParameters {
    static AFKTimeoutSecs = 'AFKTimeout' as const;
    static AFKCountdownSecs = 'AFKCountdown' as const;
    static MinQP = 'MinQP' as const;
    static MaxQP = 'MaxQP' as const;
    static WebRTCFPS = 'WebRTCFPS' as const;
    static WebRTCMinBitrate = 'WebRTCMinBitrate' as const;
    static WebRTCMaxBitrate = 'WebRTCMaxBitrate' as const;
    static MaxReconnectAttempts = 'MaxReconnectAttempts' as const;
    static StreamerAutoJoinInterval = 'StreamerAutoJoinInterval' as const;
}

export type NumericParametersKeys = Exclude<
    keyof typeof NumericParameters,
    'prototype'
>;
export type NumericParametersIds =
    typeof NumericParameters[NumericParametersKeys];

const isNumericId = (id: string): id is NumericParametersIds =>
    Object.getOwnPropertyNames(NumericParameters).some(
        (name: NumericParametersKeys) => NumericParameters[name] === id
    );

/**
 * A collection of textual parameters that are core to all Pixel Streaming experiences.
 *
 */
export class TextParameters {
    static SignallingServerUrl = 'ss' as const;
}

export type TextParametersKeys = Exclude<
    keyof typeof TextParameters,
    'prototype'
>;
export type TextParametersIds = typeof TextParameters[TextParametersKeys];

const isTextId = (id: string): id is TextParametersIds =>
    Object.getOwnPropertyNames(TextParameters).some(
        (name: TextParametersKeys) => TextParameters[name] === id
    );

/**
 * A collection of enum based parameters that are core to all Pixel Streaming experiences.
 *
 */
export class OptionParameters {
    static PreferredCodec = 'PreferredCodec' as const;
    static StreamerId = 'StreamerId' as const;
}

export type OptionParametersKeys = Exclude<
    keyof typeof OptionParameters,
    'prototype'
>;
export type OptionParametersIds = typeof OptionParameters[OptionParametersKeys];

const isOptionId = (id: string): id is OptionParametersIds =>
    Object.getOwnPropertyNames(OptionParameters).some(
        (name: OptionParametersKeys) => OptionParameters[name] === id
    );

/**
 * Utility types for inferring data type based on setting ID
 */
export type OptionIds =
    | FlagsIds
    | NumericParametersIds
    | TextParametersIds
    | OptionParametersIds;
export type OptionKeys<T> = T extends FlagsIds
    ? boolean
    : T extends NumericParametersIds
    ? number
    : T extends TextParametersIds
    ? string
    : T extends OptionParametersIds
    ? string
    : never;

export type AllSettings = {
    [K in OptionIds]: OptionKeys<K>;
};

export interface ConfigParams {
    /** Initial Pixel Streaming settings */
    initialSettings?: Partial<AllSettings>;
    /** If useUrlParams is set true, will read initial values from URL parameters and persist changed settings into URL */
    useUrlParams?: boolean;
}
export class Config {
    /* A map of flags that can be toggled - options that can be set in the application - e.g. Use Mic? */
    private flags = new Map<FlagsIds, SettingFlag>();

    /* A map of numerical settings - options that can be in the application - e.g. MinBitrate */
    private numericParameters = new Map<NumericParametersIds, SettingNumber>();

    /* A map of text settings - e.g. signalling server url */
    private textParameters = new Map<TextParametersIds, SettingText>();

    /* A map of enum based settings - e.g. preferred codec */
    private optionParameters = new Map<OptionParametersIds, SettingOption>();

    private _useUrlParams: boolean;

    // ------------ Settings -----------------

    constructor(config: ConfigParams = {}) {
        const { initialSettings, useUrlParams } = config;
        this._useUrlParams = !!useUrlParams;
        this.populateDefaultSettings(this._useUrlParams, initialSettings);
    }

    /**
     * True if reading configuration initial values from URL parameters, and
     * persisting changes in URL when changed.
     */
    public get useUrlParams() {
        return this._useUrlParams;
    }

    /**
     * Populate the default settings for a Pixel Streaming application
     */
    private populateDefaultSettings(useUrlParams: boolean, settings: Partial<AllSettings>): void {
        /**
         * Text Parameters
         */

        this.textParameters.set(
            TextParameters.SignallingServerUrl,
            new SettingText(
                TextParameters.SignallingServerUrl,
                'Signalling url',
                'Url of the signalling server',
                settings && settings.hasOwnProperty(TextParameters.SignallingServerUrl) ? 
                    settings[TextParameters.SignallingServerUrl] :
                    (location.protocol === 'https:' ? 'wss://' : 'ws://') +
                        window.location.hostname +
                        // for readability, we omit the port if it's 80
                        (window.location.port === '80' ||
                        window.location.port === ''
                            ? ''
                            : `:${window.location.port}`),
                useUrlParams
            )
        );

        this.optionParameters.set(
            OptionParameters.StreamerId,
            new SettingOption(
                OptionParameters.StreamerId,
                'Streamer ID',
                'The ID of the streamer to stream.',
                settings && settings.hasOwnProperty(OptionParameters.StreamerId) ?
                    settings[OptionParameters.StreamerId] :
                    '',
                [],
                useUrlParams
            )
        );

        /**
         * Enum Parameters
         */
        this.optionParameters.set(
            OptionParameters.PreferredCodec,
            new SettingOption(
                OptionParameters.PreferredCodec,
                'Preferred Codec',
                'The preferred codec to be used during codec negotiation',
                'H264 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f',
                settings && settings.hasOwnProperty(OptionParameters.PreferredCodec) ?
                    [settings[OptionParameters.PreferredCodec]] :
                    (function (): Array<string> {
                        const browserSupportedCodecs: Array<string> = [];
                        // Try get the info needed from the RTCRtpReceiver. This is only available on chrome
                        if (!RTCRtpReceiver.getCapabilities) {
                            browserSupportedCodecs.push('Only available on Chrome');
                            return browserSupportedCodecs;
                        }

                        const matcher = /(VP\d|H26\d|AV1).*/;
                        const codecs =
                            RTCRtpReceiver.getCapabilities('video').codecs;
                        codecs.forEach((codec) => {
                            const str =
                                codec.mimeType.split('/')[1] +
                                ' ' +
                                (codec.sdpFmtpLine || '');
                            const match = matcher.exec(str);
                            if (match !== null) {
                                browserSupportedCodecs.push(str);
                            }
                        });
                        return browserSupportedCodecs;
                    })(),
                useUrlParams
            )
        );	

        /**
         * Boolean parameters
         */

        this.flags.set(
            Flags.AutoConnect,
            new SettingFlag(
                Flags.AutoConnect,
                'Auto connect to stream',
                'Whether we should attempt to auto connect to the signalling server or show a click to start prompt.',
                settings && settings.hasOwnProperty(Flags.AutoConnect) ?
                    settings[Flags.AutoConnect] :
                    false,
                useUrlParams
            )
        );

        this.flags.set(
            Flags.AutoPlayVideo,
            new SettingFlag(
                Flags.AutoPlayVideo,
                'Auto play video',
                'When video is ready automatically start playing it as opposed to showing a play button.',
                settings && settings.hasOwnProperty(Flags.AutoPlayVideo) ?
                    settings[Flags.AutoPlayVideo] :
                    true,
                useUrlParams
            )
        );

        this.flags.set(
            Flags.BrowserSendOffer,
            new SettingFlag(
                Flags.BrowserSendOffer,
                'Browser send offer',
                'Browser will initiate the WebRTC handshake by sending the offer to the streamer',
                settings && settings.hasOwnProperty(Flags.BrowserSendOffer) ?
                    settings[Flags.BrowserSendOffer] :
                    false,
                useUrlParams
            )
        );

        this.flags.set(
            Flags.UseMic,
            new SettingFlag(
                Flags.UseMic,
                'Use microphone',
                'Make browser request microphone access and open an input audio track.',
                settings && settings.hasOwnProperty(Flags.UseMic) ?
                    settings[Flags.UseMic] :
                    false,
                useUrlParams
            )
        );

        this.flags.set(
            Flags.StartVideoMuted,
            new SettingFlag(
                Flags.StartVideoMuted,
                'Start video muted',
                'Video will start muted if true.',
                settings && settings.hasOwnProperty(Flags.StartVideoMuted) ?
                    settings[Flags.StartVideoMuted] :
                    false,
                useUrlParams
            )
        );

        this.flags.set(
            Flags.SuppressBrowserKeys,
            new SettingFlag(
                Flags.SuppressBrowserKeys,
                'Suppress browser keys',
                'Suppress certain browser keys that we use in UE, for example F5 to show shader complexity instead of refresh the page.',
                settings && settings.hasOwnProperty(Flags.SuppressBrowserKeys) ?
                    settings[Flags.SuppressBrowserKeys] :
                    true,
                useUrlParams
            )
        );

        this.flags.set(
            Flags.IsQualityController,
            new SettingFlag(
                Flags.IsQualityController,
                'Is quality controller?',
                'True if this peer controls stream quality',
                settings && settings.hasOwnProperty(Flags.IsQualityController) ?
                    settings[Flags.IsQualityController] :
                    true,
                useUrlParams
            )
        );

        this.flags.set(
            Flags.ForceMonoAudio,
            new SettingFlag(
                Flags.ForceMonoAudio,
                'Force mono audio',
                'Force browser to request mono audio in the SDP',
                settings && settings.hasOwnProperty(Flags.ForceMonoAudio) ?
                    settings[Flags.ForceMonoAudio] :
                    false,
                useUrlParams
            )
        );

        this.flags.set(
            Flags.ForceTURN,
            new SettingFlag(
                Flags.ForceTURN,
                'Force TURN',
                'Only generate TURN/Relayed ICE candidates.',
                settings && settings.hasOwnProperty(Flags.ForceTURN) ?
                    settings[Flags.ForceTURN] :
                    false,
                useUrlParams
            )
        );

        this.flags.set(
            Flags.AFKDetection,
            new SettingFlag(
                Flags.AFKDetection,
                'AFK if idle',
                'Timeout the experience if user is AFK for a period.',
                settings && settings.hasOwnProperty(Flags.AFKDetection) ?
                    settings[Flags.AFKDetection] :
                    false,
                useUrlParams
            )
        );

        this.flags.set(
            Flags.MatchViewportResolution,
            new SettingFlag(
                Flags.MatchViewportResolution,
                'Match viewport resolution',
                'Pixel Streaming will be instructed to dynamically resize the video stream to match the size of the video element.',
                settings && settings.hasOwnProperty(Flags.MatchViewportResolution) ?
                    settings[Flags.MatchViewportResolution] :
                    false,
                useUrlParams
            )
        );

        this.flags.set(
            Flags.HoveringMouseMode,
            new SettingFlag(
                Flags.HoveringMouseMode,
                'Control Scheme: Locked Mouse',
                'Either locked mouse, where the pointer is consumed by the video and locked to it, or hovering mouse, where the mouse is not consumed.',
                settings && settings.hasOwnProperty(Flags.HoveringMouseMode) ?
                    settings[Flags.HoveringMouseMode] :
                    false,
                useUrlParams,
                (isHoveringMouse: boolean, setting: SettingBase) => {
                    setting.label = `Control Scheme: ${isHoveringMouse ? 'Hovering' : 'Locked'} Mouse`;
                }
            )
        );

        this.flags.set(
            Flags.FakeMouseWithTouches,
            new SettingFlag(
                Flags.FakeMouseWithTouches,
                'Fake mouse with touches',
                'A single finger touch is converted into a mouse event. This allows a non-touch application to be controlled partially via a touch device.',
                settings && settings.hasOwnProperty(Flags.FakeMouseWithTouches) ?
                    settings[Flags.FakeMouseWithTouches] :
                    false,
                useUrlParams
            )
        );

        this.flags.set(
            Flags.KeyboardInput,
            new SettingFlag(
                Flags.KeyboardInput,
                'Keyboard input',
                'If enabled, send keyboard events to streamer',
                settings && settings.hasOwnProperty(Flags.KeyboardInput) ?
                    settings[Flags.KeyboardInput] :
                    true,
                useUrlParams
            )
        );

        this.flags.set(
            Flags.MouseInput,
            new SettingFlag(
                Flags.MouseInput,
                'Mouse input',
                'If enabled, send mouse events to streamer',
                settings && settings.hasOwnProperty(Flags.MouseInput) ?
                    settings[Flags.MouseInput] :
                    true,
                useUrlParams
            )
        );

        this.flags.set(
            Flags.TouchInput,
            new SettingFlag(
                Flags.TouchInput,
                'Touch input',
                'If enabled, send touch events to streamer',
                settings && settings.hasOwnProperty(Flags.TouchInput) ?
                    settings[Flags.TouchInput] :
                    true,
                useUrlParams
            )
        );

        this.flags.set(
            Flags.GamepadInput,
            new SettingFlag(
                Flags.GamepadInput,
                'Gamepad input',
                'If enabled, send gamepad events to streamer',
                settings && settings.hasOwnProperty(Flags.GamepadInput) ?
                    settings[Flags.GamepadInput] :
                    true,
                useUrlParams
            )
        );

        this.flags.set(
            Flags.XRControllerInput,
            new SettingFlag(
                Flags.XRControllerInput,
                'XR controller input',
                'If enabled, send XR controller events to streamer',
                settings && settings.hasOwnProperty(Flags.XRControllerInput) ?
                    settings[Flags.XRControllerInput] :
                    true,
                useUrlParams
            )
        );

        this.flags.set(
            Flags.WaitForStreamer,
            new SettingFlag(
                Flags.WaitForStreamer,
                'Wait for streamer',
                'Will continue trying to connect to the first streamer available.',
                settings && settings.hasOwnProperty(Flags.WaitForStreamer) ?
                    settings[Flags.WaitForStreamer] :
                    true,
                useUrlParams
            )
        );
        
        this.flags.set(
            Flags.HideUI,
            new SettingFlag(
                Flags.HideUI,
                'Hide the UI overlay',
                'Will hide all UI overlay details',
                settings && settings.hasOwnProperty(Flags.HideUI) ?
                    settings[Flags.HideUI] :
                    false,
                useUrlParams
            )
        );

        /**
         * Numeric parameters
         */

        this.numericParameters.set(
            NumericParameters.AFKTimeoutSecs,
            new SettingNumber(
                NumericParameters.AFKTimeoutSecs,
                'AFK timeout',
                'The time (in seconds) it takes for the application to time out if AFK timeout is enabled.',
                0 /*min*/,
                null /*max*/,
                settings && settings.hasOwnProperty(NumericParameters.AFKTimeoutSecs) ?
                    settings[NumericParameters.AFKTimeoutSecs] :
                    120, /*value*/
                useUrlParams
            )
        );

        this.numericParameters.set(
            NumericParameters.AFKCountdownSecs,
            new SettingNumber(
                NumericParameters.AFKCountdownSecs,
                'AFK countdown',
                'The time (in seconds) for a user to respond before the stream is ended after an AFK timeout.',
                10 /*min*/,
                null /*max*/,
                10 /*value*/,
                useUrlParams
            )
        )

        this.numericParameters.set(
            NumericParameters.MaxReconnectAttempts,
            new SettingNumber(
                NumericParameters.MaxReconnectAttempts,
                'Max Reconnects',
                'Maximum number of reconnects the application will attempt when a streamer disconnects.',
                0 /*min*/,
                999 /*max*/,
                settings && settings.hasOwnProperty(NumericParameters.MaxReconnectAttempts) ?
                    settings[NumericParameters.MaxReconnectAttempts] :
                    3, /*value*/
                useUrlParams
            )
        );

        this.numericParameters.set(
            NumericParameters.MinQP,
            new SettingNumber(
                NumericParameters.MinQP,
                'Min QP',
                'The lower bound for the quantization parameter (QP) of the encoder. 0 = Best quality, 51 = worst quality.',
                0 /*min*/,
                51 /*max*/,
                settings && settings.hasOwnProperty(NumericParameters.MinQP) ?
                    settings[NumericParameters.MinQP] :
                    0, /*value*/
                useUrlParams
            )
        );

        this.numericParameters.set(
            NumericParameters.MaxQP,
            new SettingNumber(
                NumericParameters.MaxQP,
                'Max QP',
                'The upper bound for the quantization parameter (QP) of the encoder. 0 = Best quality, 51 = worst quality.',
                0 /*min*/,
                51 /*max*/,
                settings && settings.hasOwnProperty(NumericParameters.MaxQP) ?
                    settings[NumericParameters.MaxQP] :
                    51, /*value*/
                useUrlParams
            )
        );

        this.numericParameters.set(
            NumericParameters.WebRTCFPS,
            new SettingNumber(
                NumericParameters.WebRTCFPS,
                'Max FPS',
                'The maximum FPS that WebRTC will try to transmit frames at.',
                1 /*min*/,
                999 /*max*/,
                settings && settings.hasOwnProperty(NumericParameters.WebRTCFPS) ?
                    settings[NumericParameters.WebRTCFPS] :
                    60, /*value*/
                useUrlParams
            )
        );

        this.numericParameters.set(
            NumericParameters.WebRTCMinBitrate,
            new SettingNumber(
                NumericParameters.WebRTCMinBitrate,
                'Min Bitrate (kbps)',
                'The minimum bitrate that WebRTC should use.',
                0 /*min*/,
                500000 /*max*/,
                settings && settings.hasOwnProperty(NumericParameters.WebRTCMinBitrate) ?
                    settings[NumericParameters.WebRTCMinBitrate] :
                    0, /*value*/
                useUrlParams
            )
        );

        this.numericParameters.set(
            NumericParameters.WebRTCMaxBitrate,
            new SettingNumber(
                NumericParameters.WebRTCMaxBitrate,
                'Max Bitrate (kbps)',
                'The maximum bitrate that WebRTC should use.',
                0 /*min*/,
                500000 /*max*/,
                settings && settings.hasOwnProperty(NumericParameters.WebRTCMaxBitrate) ?
                    settings[NumericParameters.WebRTCMaxBitrate] :
                    0, /*value*/
                useUrlParams
            )
        );

        this.numericParameters.set(
            NumericParameters.StreamerAutoJoinInterval,
            new SettingNumber(
                NumericParameters.StreamerAutoJoinInterval,
                'Streamer Auto Join Interval (ms)',
                'Delay between retries when waiting for an available streamer.',
                500 /*min*/,
                900000 /*max*/,
                settings && settings.hasOwnProperty(NumericParameters.StreamerAutoJoinInterval) ?
                    settings[NumericParameters.StreamerAutoJoinInterval] :
                    3000, /*value*/
                useUrlParams
            )
        );
    }

    /**
     * Add a callback to fire when the numeric setting is toggled.
     * @param id The id of the flag.
     * @param onChangedListener The callback to fire when the numeric value changes.
     */
    _addOnNumericSettingChangedListener(
        id: NumericParametersIds,
        onChangedListener: (newValue: number) => void
    ): void {
        if (this.numericParameters.has(id)) {
            this.numericParameters
                .get(id)
                .addOnChangedListener(onChangedListener);
        }
    }

    _addOnOptionSettingChangedListener(
        id: OptionParametersIds,
        onChangedListener: (newValue: string) => void
    ): void {
        if (this.optionParameters.has(id)) {
            this.optionParameters
                .get(id)
                .addOnChangedListener(onChangedListener);
        }
    }

    /**
     * @param id The id of the numeric setting we are interested in getting a value for.
     * @returns The numeric value stored in the parameter with the passed id.
     */
    getNumericSettingValue(id: NumericParametersIds): number {
        if (this.numericParameters.has(id)) {
            return this.numericParameters.get(id).number;
        } else {
            throw new Error(`There is no numeric setting with the id of ${id}`);
        }
    }

    /**
     * @param id The id of the text setting we are interested in getting a value for.
     * @returns The text value stored in the parameter with the passed id.
     */
    getTextSettingValue(id: TextParametersIds): string {
        if (this.textParameters.has(id)) {
            return this.textParameters.get(id).value as string;
        } else {
            throw new Error(`There is no numeric setting with the id of ${id}`);
        }
    }

    /**
     * Set number in the setting.
     * @param id The id of the numeric setting we are interested in.
     * @param value The numeric value to set.
     */
    setNumericSetting(id: NumericParametersIds, value: number): void {
        if (this.numericParameters.has(id)) {
            this.numericParameters.get(id).number = value;
        } else {
            throw new Error(`There is no numeric setting with the id of ${id}`);
        }
    }

    /**
     * Add a callback to fire when the flag is toggled.
     * @param id The id of the flag.
     * @param onChangeListener The callback to fire when the value changes.
     */
    _addOnSettingChangedListener(
        id: FlagsIds,
        onChangeListener: (newFlagValue: boolean) => void
    ): void {
        if (this.flags.has(id)) {
            this.flags.get(id).onChange = onChangeListener;
        }
    }

    /**
     * Add a callback to fire when the text is changed.
     * @param id The id of the flag.
     * @param onChangeListener The callback to fire when the value changes.
     */
    _addOnTextSettingChangedListener(
        id: TextParametersIds,
        onChangeListener: (newTextValue: string) => void
    ): void {
        if (this.textParameters.has(id)) {
            this.textParameters.get(id).onChange = onChangeListener;
        }
    }

    /**
     * Get the option which has the given id.
     * @param id The id of the option.
     * @returns The SettingOption object matching id
     */
    getSettingOption(id: OptionParametersIds): SettingOption {
        return this.optionParameters.get(id);
    }

    /**
     * Get the value of the configuration flag which has the given id.
     * @param id The unique id for the flag.
     * @returns True if the flag is enabled.
     */
    isFlagEnabled(id: FlagsIds): boolean {
        return this.flags.get(id).flag as boolean;
    }

    /**
     * Set flag to be enabled/disabled.
     * @param id The id of the flag to toggle.
     * @param flagEnabled True if the flag should be enabled.
     */
    setFlagEnabled(id: FlagsIds, flagEnabled: boolean) {
        if (!this.flags.has(id)) {
            Logger.Warning(
                Logger.GetStackTrace(),
                `Cannot toggle flag called ${id} - it does not exist in the Config.flags map.`
            );
        } else {
            this.flags.get(id).flag = flagEnabled;
        }
    }

    /**
     * Set the text setting.
     * @param id The id of the setting
     * @param settingValue The value to set in the setting.
     */
    setTextSetting(id: TextParametersIds, settingValue: string) {
        if (!this.textParameters.has(id)) {
            Logger.Warning(
                Logger.GetStackTrace(),
                `Cannot set text setting called ${id} - it does not exist in the Config.textParameters map.`
            );
        } else {
            this.textParameters.get(id).text = settingValue;
        }
    }

    /**
     * Set the option setting list of options.
     * @param id The id of the setting
     * @param settingOptions The values the setting could take
     */
    setOptionSettingOptions(
        id: OptionParametersIds,
        settingOptions: Array<string>
    ) {
        if (!this.optionParameters.has(id)) {
            Logger.Warning(
                Logger.GetStackTrace(),
                `Cannot set text setting called ${id} - it does not exist in the Config.optionParameters map.`
            );
        } else {
            this.optionParameters.get(id).options = settingOptions;
        }
    }

    /**
     * Set option enum settings selected option.
     * @param id The id of the setting
     * @param settingOptions The value to select out of all the options
     */
    setOptionSettingValue(id: OptionParametersIds, settingValue: string) {
        if (!this.optionParameters.has(id)) {
            Logger.Warning(
                Logger.GetStackTrace(),
                `Cannot set text setting called ${id} - it does not exist in the Config.enumParameters map.`
            );
        } else {
            const optionSetting = this.optionParameters.get(id);
            const existingOptions = optionSetting.options;
            if (!existingOptions.includes(settingValue)) {
                existingOptions.push(settingValue);
                optionSetting.options = existingOptions;
            }
            optionSetting.selected = settingValue;
        }
    }

    /**
     * Set the label for the flag.
     * @param id The id of the flag.
     * @param label The new label to use for the flag.
     */
    setFlagLabel(id: FlagsIds, label: string) {
        if (!this.flags.has(id)) {
            Logger.Warning(
                Logger.GetStackTrace(),
                `Cannot set label for flag called ${id} - it does not exist in the Config.flags map.`
            );
        } else {
            this.flags.get(id).label = label;
        }
    }

        /**
         * Set a subset of all settings in one function call.
         *
         * @param settings A (partial) list of settings to set
         */
        setSettings(settings: Partial<AllSettings>) {
            for (const key of Object.keys(settings)) {
                if (isFlagId(key)) {
                    this.setFlagEnabled(key, settings[key]);
                } else if (isNumericId(key)) {
                    this.setNumericSetting(key, settings[key]);
                } else if (isTextId(key)) {
                    this.setTextSetting(key, settings[key]);
                } else if (isOptionId(key)) {
                    this.setOptionSettingValue(key, settings[key]);
                }
            }
        }

    /**
     * Get all settings
     * @returns All setting values as an object with setting ids as keys
     */
    getSettings(): Partial<AllSettings> {
        const settings: Partial<AllSettings> = {};
        for (const [key, value] of this.flags.entries()) {
            settings[key] = value.flag;
        }
        for (const [key, value] of this.numericParameters.entries()) {
            settings[key] = value.number;
        }
        for (const [key, value] of this.textParameters.entries()) {
            settings[key] = value.text;
        }
        for (const [key, value] of this.optionParameters.entries()) {
            settings[key] = value.selected;
        }
        return settings;
    }

    /**
     * Get all Flag settings as an array.
     * @returns All SettingFlag objects
     */
    getFlags(): Array<SettingFlag> {
        return Array.from(this.flags.values());
    }

    /**
     * Get all Text settings as an array.
     * @returns All SettingText objects
     */
    getTextSettings(): Array<SettingText> {
        return Array.from(this.textParameters.values());
    }

    /**
     * Get all Number settings as an array.
     * @returns All SettingNumber objects
     */
    getNumericSettings(): Array<SettingNumber> {
        return Array.from(this.numericParameters.values());
    }

    /**
     * Get all Option settings as an array.
     * @returns All SettingOption objects
     */
    getOptionSettings(): Array<SettingOption> {
        return Array.from(this.optionParameters.values());
    }

    /**
     * Emit events when settings change.
     * @param eventEmitter
     */
    _registerOnChangeEvents(eventEmitter: EventEmitter) {
        for (const key of this.flags.keys()) {
            const flag = this.flags.get(key);
            if (flag) {
                flag.onChangeEmit = (newValue: boolean) =>
                    eventEmitter.dispatchEvent(
                        new SettingsChangedEvent({
                            id: flag.id,
                            type: 'flag',
                            value: newValue,
                            target: flag
                        })
                    );
            }
        }
        for (const key of this.numericParameters.keys()) {
            const number = this.numericParameters.get(key);
            if (number) {
                number.onChangeEmit = (newValue: number) =>
                    eventEmitter.dispatchEvent(
                        new SettingsChangedEvent({
                            id: number.id,
                            type: 'number',
                            value: newValue,
                            target: number
                        })
                    );
            }
        }
        for (const key of this.textParameters.keys()) {
            const text = this.textParameters.get(key);
            if (text) {
                text.onChangeEmit = (newValue: string) =>
                    eventEmitter.dispatchEvent(
                        new SettingsChangedEvent({
                            id: text.id,
                            type: 'text',
                            value: newValue,
                            target: text
                        })
                    );
            }
        }
        for (const key of this.optionParameters.keys()) {
            const option = this.optionParameters.get(key);
            if (option) {
                option.onChangeEmit = (newValue: string) =>
                    eventEmitter.dispatchEvent(
                        new SettingsChangedEvent({
                            id: option.id,
                            type: 'option',
                            value: newValue,
                            target: option
                        })
                    );
            }
        }
    }
}

/**
 * The enum associated with the mouse being locked or hovering
 */
export enum ControlSchemeType {
    LockedMouse = 0,
    HoveringMouse = 1
}
