<?php

/**
 * Class ETM_Machine_Translator
 *
 * Facilitates Machine Translation calls
 */
class ETM_Machine_Translator {
	/**
	 * @var array Settings.
	 */
	protected $settings;

	/**
	 * @var string The HTTP referer.
	 */
	protected $referer;

	/**
	 * @var ETM_URL_Converter Handles URL conversion for eTranslation API requests.
	 */
	protected $url_converter;

	/**
	 * @var ETM_Machine_Translator_Logger Handles logging for machine translation requests and responses.
	 */
	protected $machine_translator_logger;

	/**
	 * @var array Contains the language codes for supported machine translation engines.
	 */
	protected $machine_translation_codes;

	/**
	 * @var ETM_Languages Handles language-related functionalities for eTranslation Multilingual.
	 */
	protected $etm_languages;

	/**
	 * @var array Holds info regarding if API key is correc.
	 */
	protected $correct_api_key = null;

	/**
	 * @var string Holds the HTML prefix for wrapping translatable content.
	 */
	private $html_wrapper_prefix = '<div>';

	/**
	 * @var string Holds the HTML suffix for wrapping translatable content.
	 */
	private $html_wrapper_suffix = '</div>';

	/**
	 * @var array Contains punctuation marks that usually precede space.
	 */
	private $punt_preceding_space = array( ', ', '. ', '; ', ') ', ': ', '? ', '! ' );

	/**
	 * @var string Specifies the translation delimiter used for wrapping translatable content.
	 */
	public $translation_delimiter = '__EMW__';
	/**
	 * ETM_Machine_Translator constructor.
	 *
	 * @param array $settings         Settings option.
	 */
	public function __construct( $settings ) {
		$this->settings = $settings;

		$etm = ETM_eTranslation_Multilingual::get_etm_instance();
		if ( ! $this->machine_translator_logger ) {
			$this->machine_translator_logger = $etm->get_component( 'machine_translator_logger' );
		}
		if ( ! $this->etm_languages ) {
			$this->etm_languages = $etm->get_component( 'languages' );
		}
		$this->machine_translation_codes = $this->etm_languages->get_iso_codes( $this->settings['translation-languages'] );
		add_filter( 'etm_exclude_words_from_automatic_translation', array( $this, 'sort_exclude_words_from_automatic_translation_array' ), 99999, 1 );
		add_filter( 'etm_exclude_words_from_automatic_translation', array( $this, 'exclude_special_symbol_from_translation' ), 9999, 2 );
	}

	/**
	 * Whether automatic translation is available.
	 *
	 * @param array $languages
	 * @return bool
	 */
	public function is_available( $languages = array() ) {
		if ( ! empty( $this->settings['etm_machine_translation_settings']['machine-translation'] ) &&
			$this->settings['etm_machine_translation_settings']['machine-translation'] == 'yes'
		) {
			if ( empty( $languages ) ) {
				// can be used to simply know if machine translation is available
				return true;
			}

			return $this->check_languages_availability( $languages );

		} else {
			return false;
		}
	}

	public function check_languages_availability( $languages, $force_recheck = false ) {
		if ( ! method_exists( $this, 'get_supported_languages' ) || ! method_exists( $this, 'get_engine_specific_language_codes' ) || ! $this->credentials_set() ) {
			return true;
		}
		$force_recheck = ( current_user_can( 'manage_options' ) &&
			! empty( $_GET['etm_recheck_supported_languages'] ) && $_GET['etm_recheck_supported_languages'] === '1' &&
			isset( $_GET['etm_recheck_supported_languages_nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['etm_recheck_supported_languages_nonce'] ) ), 'etm_recheck_supported_languages' ) ) ? true : $force_recheck;
		$data          = get_option( 'etm_db_stored_data', array() );
		if ( isset( $_GET['etm_recheck_supported_languages'] ) ) {
			unset( $_GET['etm_recheck_supported_languages'] );
		}

		// if supported languages are not stored, fetch them and update option
		if ( empty( $data['etm_mt_supported_languages'][ $this->settings['etm_machine_translation_settings']['translation-engine'] ]['last-checked'] ) || $force_recheck || ( method_exists( $this, 'check_formality' ) && ! isset( $data['etm_mt_supported_languages'][ $this->settings['etm_machine_translation_settings']['translation-engine'] ]['formality-supported-languages'] ) ) ) {
			if ( empty( $data['etm_mt_supported_languages'] ) ) {
				$data['etm_mt_supported_languages'] = array();
			}
			if ( empty( $data['etm_mt_supported_languages'][ $this->settings['etm_machine_translation_settings']['translation-engine'] ] ) ) {
				$data['etm_mt_supported_languages'][ $this->settings['etm_machine_translation_settings']['translation-engine'] ] = array( 'languages' => array() );
			}

			$data['etm_mt_supported_languages'][ $this->settings['etm_machine_translation_settings']['translation-engine'] ]['languages'] = $this->get_supported_languages();
			if ( method_exists( $this, 'check_formality' ) ) {
				$data['etm_mt_supported_languages'][ $this->settings['etm_machine_translation_settings']['translation-engine'] ]['formality-supported-languages'] = $this->check_formality();
			}
			$data['etm_mt_supported_languages'][ $this->settings['etm_machine_translation_settings']['translation-engine'] ]['last-checked'] = date( 'Y-m-d H:i:s' );
			update_option( 'etm_db_stored_data', $data );
		}

		$languages_iso_to_check = $this->get_engine_specific_language_codes( $languages );

		$all_are_available = ! array_diff( $languages_iso_to_check, $data['etm_mt_supported_languages'][ $this->settings['etm_machine_translation_settings']['translation-engine'] ]['languages'] );

		return apply_filters( 'etm_mt_available_supported_languages', $all_are_available, $languages, $this->settings );
	}

	public function get_last_checked_supported_languages() {
		$data = get_option( 'etm_db_stored_data', array() );
		if ( empty( $data['etm_mt_supported_languages'][ $this->settings['etm_machine_translation_settings']['translation-engine'] ]['last-checked'] ) ) {
			$this->check_languages_availability( $this->settings['translation-languages'], true );
		}
		return $data['etm_mt_supported_languages'][ $this->settings['etm_machine_translation_settings']['translation-engine'] ]['last-checked'];
	}

	/**
	 * Output an SVG based on translation engine and error flag.
	 *
	 * @param bool $show_errors true to show an error SVG, false if not.
	 */
	public function automatic_translation_svg_output( $show_errors ) {
		if ( method_exists( $this, 'automatic_translate_error_check' ) ) {
			if ( $show_errors ) {
				etm_output_svg( 'error' );
			} else {
				etm_output_svg( 'check' );
			}
		}

	}

	/**
	 *
	 * @deprecated
	 * Check the automatic translation API keys for eTranslation.
	 *
	 * @param ETM_eTranslation_Multilingual $machine_translator Machine translator instance.
	 * @param string                        $translation_engine              The translation engine
	 * @param string                        $api_key                         The API key to check.
	 *
	 * @return array [ (string) $message, (bool) $error ].
	 */
	public function automatic_translate_error_check( $machine_translator, $translation_engine, $api_key ) {

		$is_error       = false;
		$return_message = '';

		switch ( $translation_engine ) {
			case 'etranslation':
				$appname  = $api_key[0];
				$password = $api_key[1];
				if ( ! isset( $appname ) || ! isset( $password ) ) {
					$is_error       = true;
					$return_message = __( 'Please enter your eTranslation credentials.', 'etranslation-multilingual' );
				} else {
					$response = $machine_translator->test_request();
					$code     = $response['response'];
					if ( 200 !== $code ) {
						$is_error           = true;
						$translate_response = etm_etranslation_response_codes( $code );
						$return_message     = $translate_response['message'];

						error_log( "Error on eTranslation request: $response. Please recheck your credentials or try again later!" );
					}
				}
				break;
			default:
				break;
		}

		$this->correct_api_key = array(
			'message' => $return_message,
			'error'   => $is_error,
		);

		return $this->correct_api_key;
	}

	// checking if the api_key is correct in order to display unsupported languages

	public function is_correct_api_key() {

		if ( method_exists( $this, 'check_api_key_validity' ) ) {
			$verification = $this->check_api_key_validity();
		} else {
			// we only need this values for automatic translate error check function for backwards compatibility

			$machine_translator = $this;
			$translation_engine = $this->settings['etm_machine_translation_settings']['translation-engine'];
			$api_key            = $this->get_api_key();
			$verification       = $this->automatic_translate_error_check( $machine_translator, $translation_engine, $api_key );
		}
		if ( $verification['error'] == false ) {
			return true;
		}
		return false;
	}


	/**
	 * Return site referer
	 *
	 * @return string
	 */
	public function get_referer() {
		if ( ! $this->referer ) {
			if ( ! $this->url_converter ) {
				$etm                 = ETM_eTranslation_Multilingual::get_etm_instance();
				$this->url_converter = $etm->get_component( 'url_converter' );
			}

			$this->referer = $this->url_converter->get_abs_home();
		}

		return $this->referer;
	}

	/**
	 * Verifies that the machine translation request is valid
	 *
	 * @param  string $target_language_code language we're looking to translate to
	 * @param  string $source_language_code language we're looking to translate from
	 * @return bool
	 */
	public function verify_request_parameters( $target_language_code, $source_language_code ) {
		if ( ! $this->credentials_set() ||
			empty( $target_language_code ) || empty( $source_language_code ) ||
			empty( $this->machine_translation_codes[ $target_language_code ] ) ||
			empty( $this->machine_translation_codes[ $source_language_code ] ) ||
			$this->machine_translation_codes[ $target_language_code ] == $this->machine_translation_codes[ $source_language_code ]
		) {
			return false;
		}

		// Method that can be extended in the child class to add extra validation
		if ( ! $this->extra_request_validations( $target_language_code ) ) {
			return false;
		}

		// Check if crawlers are blocked
		if ( ! empty( $this->settings['etm_machine_translation_settings']['block-crawlers'] ) && $this->settings['etm_machine_translation_settings']['block-crawlers'] == 'yes' && $this->is_crawler() ) {
			return false;
		}

		// Check if daily quota is met
		if ( $this->machine_translator_logger->quota_exceeded() ) {
			return false;
		}

		return true;
	}

	/**
	 * Checks whether MT provider info is set.
	 *
	 * @return boolean
	 */
	public function credentials_set() {
		$engine = $this->settings['etm_machine_translation_settings']['translation-engine'];
		if ( $engine == 'etranslation' ) {
			return is_array( $this->get_api_key() ) && ! in_array( '', $this->get_api_key() );
		} else {
			return ! empty( $this->get_api_key() );
		}
	}

	/**
	 * Verifies user agent to check if the request is being made by a crawler
	 *
	 * @return boolean
	 */
	private function is_crawler() {
		if ( ! isset( $_SERVER['HTTP_USER_AGENT'] ) ) {
			return false;
		}

		$crawlers = apply_filters( 'etm_machine_translator_crawlers', 'rambler|abacho|acoi|accona|aspseek|altavista|estyle|scrubby|lycos|geona|ia_archiver|alexa|sogou|skype|facebook|twitter|pinterest|linkedin|naver|bing|google|yahoo|duckduckgo|yandex|baidu|teoma|xing|java\/1.7.0_45|bot|crawl|slurp|spider|mediapartners|\sask\s|\saol\s' );

		return preg_match( '/' . $crawlers . '/i', sanitize_text_field( $_SERVER['HTTP_USER_AGENT'] ) );
	}

	/**
	 * Get XML placeholders for non-translatable strings
	 *
	 * @param int $count How many placeholders to generate.
	 * @return array
	 */
	private function get_placeholders( $count ) {
		$placeholders = array();
		for ( $i = 1; $i <= $count; $i++ ) {
			$placeholders[] = "<x id=\"@$i\"/>";
		}
		return $placeholders;
	}

	/**
	 * Function to be used externally
	 *
	 * @param $strings
	 * @param $target_language_code
	 * @param $source_language_code
	 * @return array
	 */
	public function translate( $strings, $target_language_code, $source_language_code = null, $is_gettext = false ) {
		if ( ! empty( $strings ) && is_array( $strings ) && method_exists( $this, 'translate_array' ) && apply_filters( 'etm_disable_automatic_translations_due_to_error', false ) === false ) {
			$prepare_response = $this->prepare_strings_for_translation( $strings );

			if ( $this->settings['etm_machine_translation_settings']['translation-engine'] === 'etranslation' ) {
				$xml_machine_strings = $this->translate_array( $prepare_response->xml_strings, $prepare_response->original_strings, $target_language_code, $source_language_code, $is_gettext );
			} else {
				$xml_machine_strings = $this->translate_array( $prepare_response->xml_strings, $target_language_code, $source_language_code );
			}

			$machine_strings_return_array = array();
			if ( ! empty( $xml_machine_strings ) ) {
				$decoded_translations = $this->process_xml_translations( $prepare_response, $xml_machine_strings );
				foreach ( $decoded_translations as $key => $machine_string ) {
					$machine_strings_return_array[ $prepare_response->original_strings[ $key ] ] = $machine_string;
				}
			}
			return $machine_strings_return_array;
		} else {
			return array();
		}
	}

	/**
	 * Prepares the source strings for machine translation by encoding certain characters and placeholders.
	 *
	 * @param array $source_strings An array of source strings to be prepared for translation.
	 * @return stdClass An object containing the prepared strings and other related data.
	 */
	public function prepare_strings_for_translation( $source_strings ) {
		/*
		 * MT providers have a problem translating these characters ( '%', '$', '#' )...
		 * For some reasons it puts spaces after them so we need to 'encode' them and decode them back.
		 * Otherwise, it breaks the page.
		 */
		$imploded_strings                             = implode( ' ', $source_strings );
		$etm_exclude_words_from_automatic_translation = apply_filters( 'etm_exclude_words_from_automatic_translation', $this->get_strings_to_encode_before_translation(), $imploded_strings );
		$placeholders                                 = $this->get_placeholders( count( $etm_exclude_words_from_automatic_translation ) );
		$shortcode_tags_to_execute                    = apply_filters( 'etm_do_these_shortcodes_before_automatic_translation', array( 'etm_language' ) );

		$strings          = array_unique( $source_strings );
		$original_strings = array_values( $strings );
		$xml_strings      = $this->translatable_values_to_xml( $strings );

		foreach ( $xml_strings as $key => $string ) {
			/*
			 * html_entity_decode is needed before replacing the character "#" from the list because characters like '&#8220;' (8220 utf8)
			 * will get an extra space after '&' which will break the character, rendering it like this: '& #8220;'.
			 */
			$xml_strings[ $key ] = str_replace( $etm_exclude_words_from_automatic_translation, $placeholders, $string );
			$xml_strings[ $key ] = etm_do_these_shortcodes( $xml_strings[ $key ], $shortcode_tags_to_execute );
		}

		$response                   = new stdClass();
		$response->xml_strings      = $xml_strings;
		$response->original_strings = $original_strings;
		$response->placeholders     = $placeholders;
		$response->encoded_words    = $etm_exclude_words_from_automatic_translation;

		return $response;
	}

	/**
	 * Processes the translated XML strings and restores spaces and placeholders to the original format.
	 *
	 * @param stdClass $prepare_response An object containing the prepared strings and related data.
	 * @param string   $xml_translations The translated XML strings to be processed.
	 * @return array An array of processed translated strings.
	 */
	public function process_xml_translations( $prepare_response, $xml_translations ) {
		// Workaround for space inserted inside placeholder tags.
		$normalized_xmls = str_replace( ' />', '/>', $xml_translations );
		// Restore spaces around placeholders removed by MT.
		$normalized_xmls = $this->restore_spaces_around_placeholders( $prepare_response->xml_strings, $normalized_xmls, $prepare_response->placeholders );
		// Replace placeholders with original values.
		$decoded_xmls = str_replace( $prepare_response->placeholders, $prepare_response->encoded_words, $normalized_xmls );
		// Convert XML translations back to a string array.
		$string_translations = $this->decode_xml_translations( $prepare_response->original_strings, $decoded_xmls );
		// Restore any removed spaces around words.
		$result = $this->arr_restore_spaces_after_translation( $prepare_response->original_strings, $string_translations );
		return $result;
	}

	/**
	 * Converts translatable values (strings) to XML format.
	 *
	 * @param array $values An array of translatable values (strings).
	 * @return array An array of strings converted to XML format.
	 */
	private function translatable_values_to_xml( $values ) {
		$xmls  = array();
		$count = 1;
		foreach ( $values as $v ) {
			$html_string = $this->wrap_as_html( $v );
			$dom         = $this->loadHtml( $html_string );

			$body  = $dom->getElementsByTagName( 'body' )[0];
			$nodes = array( $body->firstChild );

			while ( $node = array_shift( $nodes ) ) {
				if ( $node instanceof \DOMElement ) {
					// Replace each non-text node with <g> node (or x tag if self-closing), also removing all attributes.
					$newTag = $dom->createElement( $node->hasChildNodes() ? 'g' : 'x' );
					// Set id for each <g> or <x> node.
					$newTag->setAttribute( 'id', $count++ );
					// Replace node.
					$node->parentNode->replaceChild( $newTag, $node );
					// Move all children nodes to the new node.
					while ( $node->hasChildNodes() ) {
						$newTag->appendChild( $node->firstChild );
					}
					// Process children nodes next.
					$nodes = array_merge( iterator_to_array( $newTag->childNodes ), $nodes );
				}
			}
			$xmls[] = $dom->saveXML( $body->firstChild );
		}
		return $xmls;
	}

	/**
	 * Decodes the translated XML strings back to their original format.
	 *
	 * @param array  $source_values The array of original source strings.
	 * @param string $translated_values The translated XML strings to be decoded.
	 * @return array An array of decoded translated strings.
	 */
	private function decode_xml_translations( $source_values, $translated_values ) {
		$translation_count    = count( $translated_values );
		$decoded_translations = array();

		for ( $i = 0; $i < $translation_count; $i++ ) {
			// Retrieve source and translated values.
			$source      = $source_values[ $i ];
			$translation = $translated_values[ $i ];

			// Load source as an HTML document.
			$html_source  = $this->wrap_as_html( $source );
			$html_dom     = $this->loadHtml( $html_source );
			$html_wrapper = $html_dom->getElementsByTagName( 'body' )[0]->firstChild;

			// Load translation as an XML document.
			$xml         = simplexml_load_string( $translation );
			$xml_dom     = dom_import_simplexml( $xml )->ownerDocument;
			$xml_wrapper = $xml_dom->firstChild;

			// Replace text nodes in the source HTML with matching text nodes from the translation XML.
			$new_node = $this->replace_text_nodes_bottom_up( $html_wrapper, $xml_wrapper );
			// Save as XML instead of HTML to avoid URL encoding.
			$text_xml = $html_dom->saveXML( $new_node );
			// Remove XML tag.
			$text_value = preg_replace( '/\<\?xml(.*?)\?\>/', '', $text_xml );

			// Remove the wrapper.
			$html_prefix_len        = strlen( $this->html_wrapper_prefix );
			$decoded_translation    = substr( $text_value, $html_prefix_len, strlen( $text_value ) - $html_prefix_len - strlen( $this->html_wrapper_suffix ) );
			$decoded_translations[] = $decoded_translation;
		}

		return $decoded_translations;
	}

	/**
	 * Recursively replaces text nodes in the original HTML with matching text nodes from the encoded XML.
	 *
	 * @param \DOMElement $original_node The original HTML node.
	 * @param \DOMElement $encoded_node The encoded XML node.
	 * @return \DOMElement The modified original node.
	 */
	private function replace_text_nodes_bottom_up( $original_node, $encoded_node ) {
		// Recursively call this function on each child node.
		$original_child_node = $original_node->lastChild;
		$encoded_child_node  = $encoded_node->lastChild;

		while ( $original_child_node && $encoded_child_node ) {
			$this->replace_text_nodes_bottom_up( $original_child_node, $encoded_child_node );
			$original_child_node = $original_child_node->previousSibling;
			$encoded_child_node  = $encoded_child_node->previousSibling;

			// Workaround for extra spaces inserted by MT between <g> tags like: '</g> <g>'.
			while ( $original_child_node && $encoded_child_node && XML_TEXT_NODE === $encoded_child_node->nodeType && empty( trim( $encoded_child_node->nodeValue ) ) && XML_TEXT_NODE !== $original_child_node->nodeType ) {
				$textNode = $original_node->ownerDocument->createTextNode( $encoded_child_node->nodeValue );
				$original_node->insertBefore( $textNode, $original_child_node->nextSibling );
				$encoded_child_node = $encoded_child_node->previousSibling;
			}
		}

		// Process the current node.
		if ( XML_TEXT_NODE === $original_node->nodeType ) {
			$original_node->nodeValue = $encoded_node->nodeValue;
		}

		return $original_node;
	}

	/**
	 * Restores spaces around placeholders in translated XML strings that were removed by the MT provider.
	 *
	 * @param array $original_xml_values An array of original XML strings.
	 * @param array $translated_xml_values An array of translated XML strings.
	 * @param array $placeholders An array of placeholders to be restored.
	 * @return array An array of translated XML strings with restored spaces.
	 */
	private function restore_spaces_around_placeholders( $original_xml_values, $translated_xml_values, $placeholders ) {
		$original_xml   = implode( $this->translation_delimiter, $original_xml_values );
		$translated_xml = implode( $this->translation_delimiter, $translated_xml_values );

		foreach ( $placeholders as $key ) {
			$original_pos    = strpos( $original_xml, $key );
			$translation_pos = strpos( $translated_xml, $key );

			if ( $original_pos > -1 && $translation_pos > -1 ) {
				$original_char_before    = substr( $original_xml, $original_pos - 1, 1 );
				$translation_char_before = substr( $translated_xml, $translation_pos - 1, 1 );

				if ( ctype_space( $original_char_before ) && ! ctype_space( $translation_char_before ) ) {
					$translated_xml = str_replace( $key, $original_char_before . $key, $translated_xml );
					$translation_pos++;
				}

				$original_char_after    = substr( $original_xml, $original_pos + strlen( $key ), 1 );
				$translation_char_after = substr( $translated_xml, $translation_pos + strlen( $key ), 1 );

				if ( ctype_space( $original_char_after ) && ! ctype_space( $translation_char_after ) ) {
					$translated_xml = str_replace( $key, $key . $original_char_after, $translated_xml );
				}

				$translation_char_after      = substr( $translated_xml, $translation_pos + strlen( $key ), 1 );
				$original_two_chars_after    = substr( $original_xml, $original_pos + strlen( $key ), 2 );
				$translation_two_chars_after = substr( $translated_xml, $translation_pos + strlen( $key ), 2 );

				if ( in_array( $original_two_chars_after, $this->punt_preceding_space, true ) && ! in_array( $translation_two_chars_after, $this->punt_preceding_space, true ) && ! ctype_space( $translation_char_after ) ) {
					if ( trim( $original_two_chars_after ) === $translation_char_after ) {
						$translated_xml = str_replace( $key . $translation_char_after, $key . $original_two_chars_after, $translated_xml );
					} else {
						$translated_xml = str_replace( $key, $key . $original_two_chars_after, $translated_xml );
					}
				}
			}
		}

		return explode( $this->translation_delimiter, $translated_xml );
	}

	/**
	 * Wraps a string with HTML tags to be used in the translation process.
	 *
	 * @param string $string The string to be wrapped.
	 * @return string The string wrapped with HTML tags.
	 */
	private function wrap_as_html( $string ) {
		return $this->html_wrapper_prefix . $string . $this->html_wrapper_suffix;
	}

	/**
	 * Restores spaces in the translated string after translation.
	 *
	 * @param string $original The original string before translation.
	 * @param string $translation The translated string with removed spaces.
	 * @return string The translated string with restored spaces.
	 */
	private function str_restore_spaces_after_translation( $original, $translation ) {
		$space  = ' ';
		$result = $translation;

		if ( strlen( $original ) > 0 && strlen( $translation ) > 0 ) {
			if ( $original[0] == $space && $translation[0] != $space ) {
				$result = $space . $translation;
			}

			if ( str_ends_with( $original, $space ) && ! str_ends_with( $translation, $space ) ) {
				$result = $result . $space;
			}

			if ( in_array( substr( $original, 0, 2 ), $this->punt_preceding_space ) && ! in_array( substr( $translation, 0, 2 ), $this->punt_preceding_space ) ) {
				$result = substr( $original, 0, 2 ) . ltrim( $translation );
			}
		}

		return $result;
	}

	/**
	 * Restores spaces in an array of translated strings after translation.
	 *
	 * @param array $originals An array of original strings before translation.
	 * @param array $translations An array of translated strings with removed spaces.
	 * @return array An array of translated strings with restored spaces.
	 */
	private function arr_restore_spaces_after_translation( $originals, $translations ): array {
		$results = array();

		for ( $i = 0; $i < count( $originals ); $i++ ) {
			$res       = $this->str_restore_spaces_after_translation( $originals[ $i ], $translations[ $i ] );
			$results[] = $res;
		}

		return $results;
	}

	/**
	 * Loads HTML content into a DOMDocument object.
	 *
	 * @param string $html The HTML content to be loaded.
	 * @return \DOMDocument The DOMDocument object representing the loaded HTML content.
	 */
	private function loadHtml( $html ) {
		$document = <<<EOD
		<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
		<html xmlns="http://www.w3.org/1999/xhtml">
		<head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head>
		<body>!html</body>
		</html>
		EOD;

		// PHP's \DOMDocument::saveXML() encodes carriage returns as &#13; so normalize all newlines to line feeds.
		$html = str_replace(
			array(
				"\r\n",
				"\r",
			),
			"\n",
			$html
		);

		// PHP's \DOMDocument serialization adds extra whitespace when the markup
		// of the wrapping document contains newlines, so ensure we remove all
		// newlines before injecting the actual HTML body to be processed.
		$document = strtr(
			$document,
			array(
				"\n"    => '',
				'!html' => $html,
			)
		);

		$dom = new \DOMDocument();

		// Ignore warnings during HTML soup loading.
		@$dom->loadHTML( $document, LIBXML_NOBLANKS );

		return $dom;
	}

	/**
	 * Gets an array of strings that need to be encoded before translation.
	 *
	 * @return array An array of strings to be encoded before translation.
	 */
	public static function get_strings_to_encode_before_translation(): array {
		$result  = array();
		$letters = array( 's', 'd', 'f', 'u' );

		foreach ( $letters as $l ) {
			$result[] = "%$l";

			for ( $i = 1; $i < 10; $i++ ) {
				$result[] = "%$i\$$l";
			}
		}

		return array_merge( $result, array( '%', '$', '#' ) );
	}

	/**
	 * @param $etm_exclude_words_from_automatic_translation
	 * @return mixed
	 *
	 * We need to sort the $etm_exclude_words_from_automatic_translation array descending because we risk to not translate excluded multiple words when one
	 * is repeated ( example: Facebook, Facebook Store, Facebook View, because Facebook was the first one in the array it was replaced with a code and the
	 * other words group ( Store, View) were translated)
	 */
	public function sort_exclude_words_from_automatic_translation_array( $etm_exclude_words_from_automatic_translation ) {
		usort( $etm_exclude_words_from_automatic_translation, array( $this, 'sort_array' ) );

		return $etm_exclude_words_from_automatic_translation;
	}

	/**
	 * Sorts an array based on the length of its elements in descending order.
	 *
	 * @param array $a The first element for comparison.
	 * @param array $b The second element for comparison.
	 * @return int Returns a negative value if $b has a greater length than $a,
	 *             a positive value if $a has a greater length than $b,
	 *             or zero if they have equal lengths.
	 */
	public function sort_array( $a, $b ) {
		return strlen( $b ) - strlen( $a );
	}

	/**
	 * Overridable function for testing requests.
	 */
	public function test_request(){}

	/**
	 * Retrieves the API key.
	 *
	 * @return false Returns false by default, indicating that no API key is available.
	 */
	public function get_api_key() {
		return false;
	}

	/**
	 * Performs extra validations on the translation request.
	 *
	 * @param string $to_language The target language code for translation.
	 * @return bool Returns true if the extra validations pass, false otherwise.
	 */
	public function extra_request_validations( $to_language ) {
		return true;
	}

	/**
	 * Excludes special symbols from translation in an array of strings.
	 *
	 * @param array  $array   The array of symbols to exclude.
	 * @param string $strings The string to check for the presence of symbols.
	 * @return array Returns the updated array with excluded symbols.
	 */
	public function exclude_special_symbol_from_translation( $array, $strings ) {
		$float_array_symbols = array( 'd', 's', 'e', 'E', 'f', 'F', 'g', 'G', 'h', 'H', 'u' );
		foreach ( $float_array_symbols as $float_array_symbol ) {
			for ( $i = 1; $i <= 10; $i++ ) {
				$symbol = '%' . $i . '$' . $float_array_symbol;
				if ( strpos( $strings, $symbol ) !== false ) {
					$array[] = '%' . $i . '$' . $float_array_symbol;
				}
			}
		}
		return $array;
	}

	/**
	 * Retrieves all domains from the translation engine.
	 *
	 * @return array Returns an array of domain names.
	 */
	public function get_all_domains() {
		$engine_name    = $this->settings['etm_machine_translation_settings']['translation-engine'];
		$option_name    = "etm_mt_domains_$engine_name";
		$stored_domains = get_option( $option_name );
		if ( $stored_domains && ! empty( $stored_domains ) ) {
			return $stored_domains;
		} else {
			$response = $this->get_domain_list();
			if ( $response['response'] == 200 ) {
				$domains = $response['body'];
				update_option( $option_name, $domains );
				return $domains;
			}
		}
		return array();
	}

}

add_filter( 'http_request_timeout', 'wp9838c_timeout_extend' );
/**
 * Adjust request timeout
 *
 * @param int $time Current timeout.
 * @return int
 */
function wp9838c_timeout_extend( $time ) {
	if ( is_engine_etranslation() ) {
		return ETM_HTTP_REQUEST_TIMEOUT;
	} else {
		return ETM_HTTP_TRANSLATION_TIMEOUT;
	}
}

// Setting custom option values for cURL. Using a high value for priority to ensure the function runs after any other added to the same action hook.
add_action( 'http_api_curl', 'etm_custom_curl_opts', 9999, 1 );
function etm_custom_curl_opts( $handle ) {
	if ( is_engine_etranslation() ) {
		curl_setopt( $handle, CURLOPT_CONNECTTIMEOUT, ETM_HTTP_REQUEST_TIMEOUT );
		curl_setopt( $handle, CURLOPT_TIMEOUT, ETM_HTTP_REQUEST_TIMEOUT );

		curl_setopt( $handle, CURLOPT_SSL_VERIFYPEER, false );
		curl_setopt( $handle, CURLOPT_FOLLOWLOCATION, true );
	}
}

// Setting custom timeout in HTTP request args
add_filter( 'http_request_args', 'etm_custom_http_request_args', 9999, 1 );
/**
 * Modifies request timeout if engine is eTranslation.
 *
 * @param object $r request object.
 * @return object
 */
function etm_custom_http_request_args( $r ) {
	if ( is_engine_etranslation() ) {
		$r['timeout'] = ETM_HTTP_REQUEST_TIMEOUT;
	}
	return $r;
}
/**
 * Checks if selected MT engine is eTranslation.
 *
 * @return boolean
 */
function is_engine_etranslation() {
	$etm      = ETM_eTranslation_Multilingual::get_etm_instance();
	$settings = $etm->get_component( 'settings' )->get_settings();
	return 'etranslation' === $settings['etm_machine_translation_settings']['translation-engine'];
}
