<?php
/**
 * Readabler
 * Web accessibility for Your WordPress site.
 * Exclusively on https://1.envato.market/readabler
 *
 * @encoding        UTF-8
 * @version         2.0.5
 * @copyright       (C) 2018 - 2024 Merkulove ( https://merkulov.design/ ). All rights reserved.
 * @license         Envato License https://1.envato.market/KYbje
 * @contributors    Nemirovskiy Vitaliy (nemirovskiyvitaliy@gmail.com), Dmitry Merkulov (dmitry@merkulov.design)
 * @support         help@merkulov.design
 * @license         Envato License https://1.envato.market/KYbje
 **/

namespace Merkulove\Readabler;

use Merkulove\Readabler\Unity\Plugin;
use Merkulove\Readabler\Unity\Settings;
use Merkulove\Readabler\Unity\Tab;
use Merkulove\Readabler\Unity\UI;
use Google\Cloud\TextToSpeech\V1\TextToSpeechClient as TextToSpeechClient7;
use Google\Cloud\TextToSpeech\V1\Client\TextToSpeechClient as TextToSpeechClient8;
use Google\Cloud\TextToSpeech\V1\ListVoicesRequest as ListVoicesRequest8;
use Google\ApiCore\ApiException;

/** Exit if accessed directly. */
if ( ! defined( 'ABSPATH' ) ) {
	header( 'Status: 403 Forbidden' );
	header( 'HTTP/1.1 403 Forbidden' );
	exit;
}

/**
 * @package Merkulove/Readabler
 */
final class TabTextToSpeech extends Tab {

	/**
	 * @var string
	 */
	private static $key = 'text_to_speech';

	/**
	 * The one true TabTextToSpeech.
	 *
	 * @since 1.0.0
	 * @access private
	 * @var TabTextToSpeech
	 **/
	private static $instance;

	/** Text-to-Speech client */
	private static $tts_client;

	/** List of voices */
	private static $voices;

	/**
	 * @var ApiException
	 */
	private static $api_error_message;

	/**
	 * Render Select Language field.
	 *
	 * @param string $key - Field key.
	 * @param string $tab_slug - Tab slug to which the field belongs.
	 *
	 * @return void
	 *
	 * @noinspection PhpUnused
	 **@throws ApiException
	 * @throws ApiException
	 * @since  1.0.0
	 * @access public
	 *
	 */
	public function render_language( string $key, string $tab_slug ) {

		/** Prepare Languages Options. */
		$languages   = [];
		$languages[] = esc_html__( 'Select Language', 'readabler' );
		foreach ( self::$voices as $voice ) {
			$lang               = Language::get_lang_by_code( $voice->getLanguageCodes() );
			$languages[ $lang ] = $lang;
		}
		ksort( $languages );

		/** Render Language select. */
		UI::get_instance()->render_select(
			$languages,
			'',
			esc_html__( 'Language', 'readabler' ),
			'',
			[
				'name' => 'mdp_readabler_language_filter',
				'id'   => 'mdp-readabler-language-filter'
			]
		);

		/** Shorthand for plugin settings. */
		$options = Settings::get_instance()->options;

		?>

        <div class="mdc-text-field-helper-line mdp-readabler-helper-padding">
            <div class="mdc-text-field-helper-text mdc-text-field-helper-text--persistent"><?php esc_html_e( 'The list includes both standard and', 'readabler' ); ?>
                <a href="https://cloud.google.com/text-to-speech/docs/wavenet"
                   target="_blank"><?php esc_html_e( 'WaveNet voices', 'readabler' ); ?></a>.
				<?php esc_html_e( 'WaveNet voices are higher quality voices with different', 'readabler' ); ?>
                <a href="https://cloud.google.com/text-to-speech/pricing"
                   target="_blank"><?php esc_html_e( 'pricing', 'readabler' ); ?></a>;
				<?php esc_html_e( 'in the list, they have the voice type "WaveNet".', 'readabler' ); ?>
            </div>
        </div>

        <table id="mdp-readabler-settings-language-tbl" class="display stripe hidden">
            <thead>
            <tr>
                <th><?php esc_html_e( 'Language', 'readabler' ); ?></th>
                <th><?php esc_html_e( 'Voice', 'readabler' ); ?></th>
                <th><?php esc_html_e( 'Gender', 'readabler' ); ?></th>
            </tr>
            </thead>
            <tbody>
			<?php foreach ( self::$voices as $voice ) : ?>
                <tr <?php if ( $voice->getName() === $options['language'] ) {
					echo 'class="selected"';
				} ?>>
                    <td class="mdp-lang-name">
						<?php echo esc_html( Language::get_lang_by_code( $voice->getLanguageCodes() ) ); // Language. ?>
                    </td>
					<?php $voice_props = self::voice_props( $voice ); ?>
                    <td>
                        <span class="mdp-lang-code" title="<?php echo esc_attr( $voice_props['lang_code'] );?>"><?php echo esc_html( $voice_props['lang_code'] ); ?></span>
                        -
                        <span><?php echo $this->get_voice_type( $voice->getName() );?></span>
                        -
                        <span class="mdp-voice-name" title="<?php echo esc_attr( $voice_props['full_name'] );?>"><?php echo esc_html( $voice_props['variant'] );?></span>
                    </td>
                    <td>
						<?php
						/** SSML voice gender values from TextToSpeech\V1\SsmlVoiceGender. */
						$ssmlVoiceGender = [ 'SSML_VOICE_GENDER_UNSPECIFIED', 'Male', 'Female', 'Neutral' ];

						echo '<span title="' . esc_attr( $ssmlVoiceGender[ $voice->getSsmlGender() ] ) . '"><img src="' . Plugin::get_url() . 'images/' . strtolower( $ssmlVoiceGender[ $voice->getSsmlGender() ] ) . '.svg" alt="' . esc_attr( $ssmlVoiceGender[ $voice->getSsmlGender() ] ) . '">' . esc_html( $ssmlVoiceGender[ $voice->getSsmlGender() ] ) . '</span>'; ?>
                    </td>
                </tr>
			<?php endforeach;

			self::$tts_client->close();

			?>
            </tbody>

        </table>

        <input id="mdp-readabler-settings-language" type='hidden' name='mdp_readabler_text_to_speech_settings[language]'
               value='<?php echo esc_attr( $options['language'] ); ?>'>
        <!--        <input id="mdp-readabler-settings-language-code" type='hidden' name='mdp_readabler_text_to_speech_settings[language-code]'-->
        <!--               value='--><?php //echo esc_attr( $options['language-code'] ); ?><!--'>-->
		<?php

		/** Restore previous exception handler. */
		restore_exception_handler();

	}

	/**
     * Get voice properties.
	 * @param $voice
	 *
	 * @return array
	 */
	private static function voice_props( $voice ): array {

		// Get language code
		$lang_code = $voice->getLanguageCodes()[0] ?? '';

		// Get full name and short name
		$full_name  = $voice->getName();
		$short_name = str_replace( $lang_code . '-', '', $full_name );

		// Split by last - to get the variant
		$variant = explode( '-', $short_name );
		$variant = $variant[ count( $variant ) - 1 ];

		// Get name without variant
		$name = str_replace( '-' . $variant, '', $short_name );

		return [
            'full_name' => $full_name,
			'lang_code' => $lang_code,
			'variant'   => $variant,
			'name'      => $name
		];

	}

	/**
	 * Return Voice Type.
	 *
	 * @param $lang_name - Google voice name.
	 *
	 * @return string
	 * @since 1.0.0
	 * @access public
	 **/
	private function get_voice_type( $lang_name ): string
	{

		// Split the string by the hyphen character
		$parts = explode( '-', $lang_name );

		// Remove first 2 elements and last one
		array_shift( $parts );
		array_shift( $parts );
		array_pop( $parts );

		// Join the remaining elements with a hyphen
		$type = implode( '-', $parts );

		// Return with icon if exists
		$icons = ['wavenet', 'neural', 'news', 'studio', 'journey', 'casual', 'polyglot', 'standard', 'neural2', 'chirp3-hd', 'chirp-hd', 'chirp'];
		if ( in_array( strtolower($type), $icons ) ) {
			return wp_sprintf(
				'<img src="%s" alt="%s">%s',
				Plugin::get_url() . 'images/voices/' . strtolower($type) . '.svg',
				esc_attr( $type ),
				esc_html( $type )
			);
		}

		return esc_html( $type );

	}

	/**
	 * Render Current Language field.
	 * @return void
	 */
	public function render_current_language() {

		/** Shorthand for options. */
		$options = Settings::get_instance()->options;

		?>
        <div class="mdp-now-used">
            <div>
                <strong><?php echo esc_attr( $options['language'] ); ?></strong>
            </div>
            <div>
                <audio controls="">
                    <source src="https://cloud.google.com/text-to-speech/docs/audio/<?php echo esc_attr( $options['language'] ); ?>.mp3"
                            type="audio/mp3">
                    <source src="https://cloud.google.com/text-to-speech/docs/audio/<?php echo esc_attr( $options['language'] ); ?>.wav"
                            type="audio/wav">
					<?php esc_html_e( 'Your browser does not support the audio element.', 'readabler' ); ?>
                </audio>
            </div>
        </div>
		<?php

	}

	/**
	 * Render Drag and Drop field.
	 *
	 * @param string $key - Field key.
	 * @param string $tab_slug - Tab slug to which the field belongs.
	 *
	 * @return void
	 *
	 * @noinspection PhpUnused
	 */
	public function render_api_key( string $key, string $tab_slug ) {

		// Check PHP version
		if ( ! Tools::is_versions_compatible() ) {
			self::php_error_message();
		}

		$options   = Settings::get_instance()->options;
		$key_exist = Tools::is_key_exists();

		?>
        <div class="mdp-dnd">
            <!--suppress HtmlFormInputWithoutLabel -->
            <div class="mdc-text-field mdc-input-width mdc-text-field--outlined mdc-hidden">
                <!--suppress HtmlFormInputWithoutLabel -->
                <input type="text"
                       class="mdc-text-field__input"
                       name="mdp_readabler_text_to_speech_settings[api_key]"
                       id="mdp-readabler-settings-dnd-api-key"
                       value="<?php echo esc_attr( $options['api_key'] ); ?>"
                >
                <div class="mdc-notched-outline mdc-notched-outline--upgraded mdc-notched-outline--notched">
                    <div class="mdc-notched-outline__leading"></div>
                    <div class="mdc-notched-outline__notch">
                        <label for="mdp-readabler-settings-dnd-api-key"
                               class="mdc-floating-label mdc-floating-label--float-above"><?php esc_html_e( 'API Key', 'readabler' ); ?></label>
                    </div>
                    <div class="mdc-notched-outline__trailing"></div>
                </div>
            </div>
            <div id="mdp-api-key-drop-zone" class="<?php if ( $key_exist ) : ?>mdp-key-uploaded<?php endif; ?>">
				<?php if ( $key_exist ) : ?>
                    <span class="material-icons">check_circle_outline</span><?php esc_html_e( 'API Key file exist', 'readabler' ); ?>
                    <span class="mdp-drop-zone-hover"><?php esc_html_e( 'Drop Key file here or click to upload', 'readabler' ); ?></span>
				<?php else : ?>
                    <span class="material-icons">cloud</span><?php esc_html_e( 'Drop Key file here or click to upload.', 'readabler' ); ?>
				<?php endif; ?>
            </div>
			<?php if ( $key_exist ) : ?>
                <div class="mdp-messages mdc-text-field-helper-line mdc-text-field-helper-text mdc-text-field-helper-text--persistent">
					<?php esc_html_e( 'Drag and drop or click on the form to replace API key. |', 'readabler' ); ?>
                    <a href="#" class="mdp-reset-key-btn"><?php esc_html_e( 'Reset API Key', 'readabler' ); ?></a>
                </div>
			<?php else : ?>
                <div class="mdp-messages mdc-text-field-helper-line mdc-text-field-helper-text mdc-text-field-helper-text--persistent">
					<?php esc_html_e( 'Drag and drop or click on the form to add ', 'readabler' ); ?><a
                            href="https://console.cloud.google.com/cloud-resource-manager"
                            target="_blank"><?php esc_html_e( 'API Key', 'readabler' ); ?>.</a>
                </div>
			<?php endif; ?>
            <input id="mdp-dnd-file-input" type="file" name="name" class="mdc-hidden"/>
        </div>
		<?php

	}

	/**
	 * Require a TTS library and get a list of voices.
	 * @return bool
	 * @throws ApiException
	 */
	private static function require_tts(): bool {

		if ( ! Tools::is_key_exists() ) {
			return false;
		}

		/** Check PHP version and vendor version */
		if ( ! Tools::is_versions_compatible() ) {
			return false;
		}

		/** Includes the autoloader for libraries installed with Composer. */
		require_once Tools::get_vendor_path();

		/** Setting custom exception handler. */
		set_exception_handler( [ ErrorHandler::class, 'exception_handler' ] );

		/** Get list of voices and return voices availability status */
		return self::tts_list_voices() !== false;

	}

	/**
	 * Get list of voices and return voices availability status
	 * @return mixed
	 */
	public static function tts_list_voices(): bool {

		$vendor_version = Tools::get_vendor_version();

		switch ( $vendor_version ) {

			case 7:

				/** Create TextToSpeechClient */
				self::$tts_client = new TextToSpeechClient7();

				/** Get a list of voices from transient */
				if ( self::tts_list_voices_transient() ) {
					return true;
				}

				/** Perform list voices request. */
				try {
					$response = self::$tts_client->listVoices();
				} catch ( ApiException $e ) {
					self::$api_error_message = $e;

					return false;
				} finally {
					self::$tts_client->close();
				}

				break;

			case 8:
			default:

				/** Create TextToSpeechClient */

				self::$tts_client = new TextToSpeechClient8();

				/** Get a list of voices from transient */
				if ( self::tts_list_voices_transient() ) {
					return true;
				}

				/** Perform list voices request. */
				try {
					$response = self::$tts_client->listVoices( new ListVoicesRequest8() );
				} catch ( ApiException $e ) {
					self::$api_error_message = $e;

					return false;
				} finally {
					self::$tts_client->close();
				}

				break;

		}

		/** Get a voice list */
		$voices = $response->getVoices();

		/** Show a warning if it was not possible to get a list of voices. */
		if ( count( $voices ) === 0 ) {

			self::voice_list_error_message();

			return false;

		}

		/** Set transient for 12 hours to reduce request count */
		set_transient( 'readabler_list_voices', $voices, 12 * HOUR_IN_SECONDS );

		self::$voices = $voices;

		return true;

	}

	/**
	 * Get a list of voices from transient.
	 *
	 * @return bool Returns true if the list of voices is retrieved from transient, false otherwise.
	 */
	private static function tts_list_voices_transient(): bool {

		/** Get a list of voices from transient */
		$transient_list_voices = get_transient( 'readabler_list_voices' );
		if ( $transient_list_voices ) {
			self::$voices = $transient_list_voices;

			return true;
		}

		return false;

	}

	/**
	 * Error message for a voice list.
	 * @return void
	 */
	private static function voice_list_error_message() {

		if ( ! is_admin() ) {
			return;
		}

		?>
        <div class="mdp-alert-error"><?php

		esc_html_e( 'Failed to get the list of languages. 
            The request failed. It looks like a problem with your API Key File. 
            Make sure that you are using the correct key file, and that the quotas have not been exceeded. 
            If you set security restrictions on a key, make sure that the current domain is added to the exceptions.', 'speaker' );

		?></div><?php

	}

	/**
	 * Controls for Text to Speech tab.
	 * @return void
	 * @throws ApiException
	 */
	public static function controls( $options ) {

        if ( ! self::showTab() ) {
            return;
        }

		$tabs   = Plugin::get_tabs();
		$fields = array();
		$dID    = 0;

		// Speaker PHP 7 conditional fields
		if ( is_plugin_active( 'readabler-php7/readabler-php7.php' ) && is_plugin_active( 'readabler-ai/readabler-ai.php' ) ) {

			$fields[ self::$key . '_divider_' . $dID++ ] = [
				'type'              => 'divider',
			];

			$php_version = PHP_VERSION;
			$fields[ 'vendor_version' ] = [
				'type'              => 'select',
				'label'             => esc_html__( 'Compatibility', 'readabler' ),
				'placeholder'       => esc_html__( 'Compatibility', 'readabler' ),
				'description'       => wp_sprintf(
				/* translators: %1$s: bold text, %2$s: br, %3$s: b */
					esc_html__( 'Select a compatible Google Library version. Current server PHP version: %1$s. %2$s Attention: Incorrect selection of the version libraries of the version will throw a %3$s on the entire site.', 'readabler' ),
					'<strong>' . esc_attr( $php_version ) . '</strong>',
					'<br>',
					'<b style="color: red">' . esc_html__( 'Fatal Error', 'readabler' ) . '</b>'
				),
				'default'           => '8.1',
				'options'           => [
					'7.4'           => 'For PHP 7.4+',
					'8.1'           => 'For PHP 8.1+',
				]
			];
		}

		if ( Tools::is_key_exists() && self::require_tts() ) {

			$fields['multi'] = [
				'type'        => 'switcher',
				'label'       => esc_html__( 'Multilingual website', 'readabler' ),
				'description' => esc_html__( 'The Site locale will be used to generate speech. Standard-A voice will be used.', 'readabler' ),
				'default'     => 'off',
			];

			$fields['current_language'] = [
				'type'    => 'player',
				'render'  => [ TabTextToSpeech::get_instance(), 'render_current_language' ],
				'label'   => esc_html__( 'Now used', 'readabler' ) . ':',
				'default' => '',
			];

			$fields['language'] = [
				'type'    => 'player',
				'render'  => [ TabTextToSpeech::get_instance(), 'render_language' ],
				'label'   => esc_html__( 'Select Language', 'readabler' ) . ':',
				'default' => 'en-US-Standard-D',
			];

			$fields['language-code'] = [
				'type'        => 'text',
				'label'       => esc_html__( 'Language Code', 'readabler' ),
				'placeholder' => esc_html__( 'Language Code', 'readabler' ),
				'default'     => 'en-US',
				'attr'        => [
					'class' => 'mdp-hidden',
					'id'    => 'mdp-readabler-settings-language-code',
				]
			];

			$fields['audio-profile'] = [
				'type'        => 'select',
				'label'       => esc_html__( 'Audio Profile', 'readabler' ) . ':',
				'placeholder' => esc_html__( 'Audio Profile', 'readabler' ),
				'description' => esc_html__( 'Optimize the synthetic speech for playback on different types of hardware.', 'readabler' ),
				'default'     => 'handset-class-device',
				'options'     => [
					'wearable-class-device'                 => esc_html__( 'Smart watches and other wearables', 'readabler' ),
					'handset-class-device'                  => esc_html__( 'Smartphones', 'readabler' ),
					'headphone-class-device'                => esc_html__( 'Earbuds or headphones', 'readabler' ),
					'small-bluetooth-speaker-class-device'  => esc_html__( 'Small home speakers', 'readabler' ),
					'medium-bluetooth-speaker-class-device' => esc_html__( 'Smart home speakers', 'readabler' ),
					'large-home-entertainment-class-device' => esc_html__( 'Home entertainment systems', 'readabler' ),
					'large-automotive-class-device'         => esc_html__( 'Car speakers', 'readabler' ),
					'telephony-class-application'           => esc_html__( 'Interactive Voice Response', 'readabler' ),
				]
			];

			$key            = 'speaking-rate';
			$fields[ $key ] = [
				'type'        => 'slider',
				'label'       => esc_html__( 'Speaking Speed', 'readabler' ) . ':',
				'description' => esc_html__( 'Speaking rate', 'readabler' ) . ':' .
				                 ' <strong>' .
				                 esc_html( ( ! empty( $options[ $key ] ) ) ? $options[ $key ] : 1 ) .
				                 '</strong>',
				'min'         => 0.25,
				'max'         => 4.0,
				'step'        => 0.25,
				'default'     => 1,
				'discrete'    => false,
			];

			$key            = 'pitch';
			$fields[ $key ] = [
				'type'        => 'slider',
				'label'       => esc_html__( 'Pitch', 'readabler' ) . ':',
				'description' => esc_html__( 'Current pitch', 'readabler' ) . ':' .
				                 ' <strong>' .
				                 esc_html( ( ! empty( $options[ $key ] ) ) ? $options[ $key ] : 0 ) .
				                 '</strong>',
				'min'         => - 20,
				'max'         => 20,
				'step'        => 0.1,
				'default'     => 0,
				'discrete'    => false,
			];

			$fields[ 'divider_' . $dID . '_' . self::$key ] = [ 'type' => 'divider' ];

			$fields['text_to_speech_bg_color'] = [
				'type'        => 'colorpicker',
				'label'       => esc_html__( 'Background Color', 'readabler' ) . ':',
				'placeholder' => esc_html__( 'Background Color', 'readabler' ),
				'description' => esc_html__( 'Select background color for text to speech tooltip.', 'readabler' ),
				'default'     => 'rgba(33, 111, 243, 1)',
				'attr'        => [
					'readonly' => 'readonly',
				]
			];

			$fields['text_to_speech_icon_color'] = [
				'type'        => 'colorpicker',
				'label'       => esc_html__( 'Icon Color', 'readabler' ) . ':',
				'placeholder' => esc_html__( 'Icon Color', 'readabler' ),
				'description' => esc_html__( 'Select icon color for text to speech tooltip.', 'readabler' ),
				'default'     => '#ffffff',
				'attr'        => [
					'readonly' => 'readonly',
				]
			];

			$fields[ 'divider_' . $dID . '_' . self::$key ] = [ 'type' => 'divider' ];

			$fields['voice_guide'] = [
				'type'        => 'text',
				'label'       => esc_html__( 'Voice guide', 'readabler' ),
				'placeholder' => esc_html__( 'Voice guide', 'readabler' ),
				'default'     => esc_html__( 'Highlight a piece of text and click Play to listen', 'readabler' ),
				'description' => esc_html__( 'This piece of text will be read and played immediately after activating Text to Speech function', 'readabler' ),
			];

			$fields[ 'divider_' . $dID . '_' . self::$key ] = [ 'type' => 'divider' ];

			$fields['highlight_p'] = [
				'type'        => 'switcher',
				'label'       => esc_html__( 'Highlight a paragraph', 'readabler' ),
				'description' => esc_html__( 'Highlight entire paragraph("p" HTML tag) when clicking on paragraph', 'readabler' ),
				'default'     => 'off',
			];

			$fields['tts_read_alt'] = [
				'type'        => 'switcher',
				'label'       => esc_html__( 'Read Alt Text', 'readabler' ),
				'description' => esc_html__( 'Reading alternative text for images', 'readabler' ),
				'default'     => 'off',
			];

			$fields[ 'divider_' . $dID . '_' . self::$key ] = [ 'type' => 'divider' ];

		}

		$fields['api_key'] = [
			'type'    => 'dragdrop',
			'render'  => [ TabTextToSpeech::get_instance(), 'render_api_key' ],
			'label'   => esc_html__( 'API Key File', 'readabler' ) . ':',
			'default' => '',
		];

		$tabs[ self::$key ]['fields'] = $fields;
		Plugin::set_tabs( $tabs );

	}

	/**
	 * Add tab to the settings page.
	 * @return void
	 */
	public static function tab() {

		if ( ! self::showTab() ) {
			return;
		}

		Tab::add_settings_tab(
			self::$key,
			6,
			'settings_voice',
			esc_html__( 'Text to Speech', 'readabler' ),
			esc_html__( 'Text to Speech Settings', 'readabler' ),
			esc_html__( 'Set up the Text to Speech feature to make your site more accessible.', 'readabler' ),
		);

	}

	/**
	 * Error message for PHP version
	 * @return void
	 */
	private static function php_error_message(): void {

		if ( ! is_admin() ) {
			return;
		}

		$message = wp_sprintf( "Plugin requires PHP 8.1 or later. Your current PHP version is %s. To ensure optimal performance and compatibility, please update your PHP version.",
			PHP_VERSION
		);

		?>
        <div class="mdp-alert-error"><?php
		echo esc_html( $message );
		?></div><?php

	}

	/**
     * Check if the tab should be shown. Is AI Assistant enabled?
	 * @return bool
	 */
    private static function showTab(): bool {

	    if ( Tools::is_readabler_ai() ) {
		    $ai_options = get_option( 'mdp_readabler_ai_settings', [] );
		    $is_google_speech = ($ai_options[ 'ai_google_speech' ] ?? 'off') === 'on';
		    if ( !$is_google_speech ) {
			    return false;
		    }
	    } else {
            $options = Settings::get_instance()->options ?? [];
            if ( ( $options['text_to_speech'] ?? 'off') !== 'on' ) {
                return false;
            }
		    if ( 'on' !== $options['text_to_speech'] && 'on' !== $options['profile_blind_users'] ) {
			    return false;
		    }
	    }

        return true;
    }

	/**
	 * Main TabTextToSpeech Instance.
	 * Insures that only one instance of TabTextToSpeech exists in memory at any one time.
	 *
	 * @static
	 * @return TabTextToSpeech
	 **/
	public static function get_instance(): TabTextToSpeech {
		if ( ! isset( self::$instance ) && ! ( self::$instance instanceof self ) ) {
			self::$instance = new self;
		}

		return self::$instance;
	}

}
