<?php
/**
 * WPML_Register_String_Filter class file.
 *
 * @package WPML\ST
 */

use WPML\ST\StringsFilter\Translator;

/**
 * Class WPML_Register_String_Filter
 */
class WPML_Register_String_Filter extends WPML_Displayed_String_Filter {

	/**
	 * WP DB instance.
	 *
	 * @var wpdb
	 */
	protected $wpdb;

	/** @var SitePress */
	protected $sitepress;

	/**
	 * @var array
	 */
	private $excluded_contexts = array();

	/**
	 * @var WPML_WP_Cache
	 */
	private $registered_string_cache;

	/** @var  WPML_ST_String_Factory $string_factory */
	private $string_factory;

	/**
	 * @var WPML_Autoregister_Save_Strings
	 */
	private $save_strings;

	// Current string data.
	protected $name;
	protected $domain;
	protected $gettext_context;
	protected $name_and_gettext_context;
	protected $key;

	/** @var bool $block_save_strings */
	private $block_save_strings = false;

	/**
	 * Domains with more than PRELOAD_LIMIT strings — skipped for bulk preload.
	 *
	 * @var array
	 */
	private $large_domains = [];

	const PRELOAD_LIMIT = 1000;

	/**
	 * @param wpdb                                $wpdb
	 * @param SitePress                           $sitepress
	 * @param WPML_ST_String_Factory              $string_factory
	 * @param Translator                          $translator
	 * @param array                               $excluded_contexts
	 * @param WPML_Autoregister_Save_Strings|null $save_strings
	 */
	public function __construct(
		$wpdb,
		SitePress $sitepress,
		&$string_factory,
		Translator $translator,
		array $excluded_contexts = array(),
		?WPML_Autoregister_Save_Strings $save_strings = null
	) {
		parent::__construct( $translator );

		$this->wpdb                    = $wpdb;
		$this->sitepress               = $sitepress;
		$this->string_factory          = &$string_factory;
		$this->excluded_contexts       = $excluded_contexts;
		$this->save_strings            = $save_strings;
		$this->registered_string_cache = new WPML_WP_Cache( 'WPML_Register_String_Filter' );
	}

	public function translate_by_name_and_context(
		$untranslated_text,
		$name,
		$context = '',
		&$has_translation = null
	) {
		if ( is_array( $untranslated_text ) || is_object( $untranslated_text ) ) {
			return '';
		}

		$translation     = $this->get_translation( $untranslated_text, $name, $context );
		$has_translation = $translation->hasTranslation();

		return $translation->getValue();
	}

	public function force_saving_of_autoregistered_strings() {
		$this->get_save_strings()->shutdown();
	}

	public function register_string( $context, $name, $value, $allow_empty_value = false, $source_lang = '' ) {

		$name = trim( $name ) ? $name : md5( $value );
		$this->initialize_current_string( $name, $context );

		/*
		 cpt slugs - do not register them when scanning themes and plugins
		 * if name starting from 'URL slug: '
		 * and context is different from 'WordPress'
		 */
		if ( substr( $name, 0, 10 ) === 'URL slug: ' && WPML_Slug_Translation::STRING_DOMAIN !== $context ) {
			return false;
		}

		list( $domain, $context, $key ) = $this->key_by_name_and_context( $name, $context );
		list( $name, $context )         = $this->truncate_name_and_context( $name, $context );

		if ( $source_lang == '' ) {
			$source_lang = $this->get_save_strings()->get_source_lang( $name, $domain );
		}

		$res = $this->get_registered_string( $domain, $context, $name );
		if ( $res ) {
			$string_id = $res['id'];

			$update_string = array();
			if ( $value != $res['value'] ) {
				$update_string['value'] = $value;
			}
			$existing_lang = $this->string_factory->find_by_id( $res['id'] )->get_language();
			if ( ! empty( $update_string ) ) {
				if ( $existing_lang == $source_lang ) {
					$this->wpdb->update( $this->wpdb->prefix . 'icl_strings', $update_string, array( 'id' => $string_id ) );
					$this->wpdb->update(
						$this->wpdb->prefix . 'icl_string_translations',
						array( 'status' => ICL_TM_NEEDS_UPDATE ),
						array( 'string_id' => $string_id )
					);
					icl_update_string_status( $string_id );
				} else {
					$orig_data               = array(
						'string_id' => $string_id,
						'language'  => $source_lang,
					);
					$update_string['status'] = ICL_TM_COMPLETE;
					if ( $this->wpdb->get_var(
						$this->wpdb->prepare(
							"SELECT COUNT(*)
																	  FROM {$this->wpdb->prefix}icl_string_translations
																	  WHERE string_id = %d
																	  	AND language = %s",
							$string_id,
							$source_lang
						)
					)
					) {
						$this->wpdb->update(
							$this->wpdb->prefix . 'icl_string_translations',
							$update_string,
							$orig_data
						);
					} else {
						$this->wpdb->insert(
							$this->wpdb->prefix . 'icl_string_translations',
							array_merge( $update_string, $orig_data )
						);
					}
					icl_update_string_status( $string_id );
				}
			}
		} else {
			$string_id = $this->save_string( $value, $allow_empty_value, $source_lang, $domain, $context, $name );
		}

		return $string_id;
	}

	/**
	 * @param string $domain
	 * @param string $context
	 * @param string $name
	 *
	 * @return array|false
	 */
	private function get_registered_string( $domain, $context, $name ) {
		$key   = md5( $domain . $name . $context );
		$found = false;

		$domain_cache = $this->registered_string_cache->get( $domain, $found );

		if ( ! $found && ! isset( $this->large_domains[ $domain ] ) ) {
			$this->maybe_preload_domain( $domain );
			$domain_cache = $this->registered_string_cache->get( $domain, $found );
		}

		if ( $found ) {
			$result = $domain_cache->get( $key, $found );
			if ( $found ) {
				return $result;
			}

			// Domain was fully preloaded — a cache miss means the string is not in the DB.
			if ( ! isset( $this->large_domains[ $domain ] ) ) {
				return false;
			}
		}

		// Large domain: fall back to a single per-string query.
		$row = $this->wpdb->get_row(
			$this->wpdb->prepare(
				"SELECT id, value FROM {$this->wpdb->prefix}icl_strings WHERE domain_name_context_md5 = %s",
				$key
			),
			ARRAY_A
		);

		if ( $row ) {
			$cached_value = [
				'id'    => $row['id'],
				'value' => $row['value'],
			];
			$this->get_domain_cache( $domain )->set( $key, $cached_value );
			return $cached_value;
		}

		return false;
	}

	/**
	 * Attempts to preload all strings for a domain in one query.
	 * If the domain exceeds PRELOAD_LIMIT strings it is flagged as large and
	 * per-string queries are used instead.
	 *
	 * @param string $domain
	 */
	private function maybe_preload_domain( $domain ) {
		$rows = $this->wpdb->get_results(
			$this->wpdb->prepare(
				"SELECT id, value, gettext_context, name FROM {$this->wpdb->prefix}icl_strings WHERE context = %s LIMIT %d",
				$domain,
				self::PRELOAD_LIMIT + 1
			),
			ARRAY_A
		);

		if ( count( (array) $rows ) > self::PRELOAD_LIMIT ) {
			$this->large_domains[ $domain ] = true;
			return;
		}

		$domain_cache = new WPML_WP_Cache( 'WPML_Register_String_Filter::' . $domain );

		foreach ( (array) $rows as $row ) {
			$key = md5( $domain . $row['name'] . $row['gettext_context'] );
			$domain_cache->set(
				$key,
				[
					'id'    => $row['id'],
					'value' => $row['value'],
				]
			);
		}

		$this->registered_string_cache->set( $domain, $domain_cache );
	}

	private function save_string( $value, $allow_empty_value, $language, $domain, $context, $name ) {
		$value = is_null( $value ) ? '' : $value;

		if ( ! $this->block_save_strings && ( $allow_empty_value || 0 !== strlen( $value ) ) ) {

			$args = array(
				'language'                => $language,
				'context'                 => $domain,
				'gettext_context'         => $context,
				'domain_name_context_md5' => md5( $domain . $name . $context ),
				'name'                    => $name,
				'value'                   => $value,
				'status'                  => ICL_TM_NOT_TRANSLATED,
			);

			$query_values = array( '%s', '%s', '%s', '%s', '%s', '%s', '%d' );

			if ( class_exists( 'WPML_TM_Translation_Priorities' ) ) {
				$args['translation_priority'] = WPML_TM_Translation_Priorities::DEFAULT_TRANSLATION_PRIORITY_VALUE_SLUG;
				$query_values[]               = '%s';
			}

			$query_values  = implode( ', ', $query_values );
			$query_columns = implode( ', ', array_keys( $args ) );
			$query         = "INSERT IGNORE INTO {$this->wpdb->prefix}icl_strings ({$query_columns}) VALUES ( {$query_values} )";

			$this->wpdb->query(
				$this->wpdb->prepare( $query, $args )
			);

			$string_id = $this->wpdb->insert_id;

			if ( $string_id === 0 ) {
				if ( empty( $this->wpdb->last_error ) ) {
					$string_id = $this->get_string_id_registered_in_concurrent_request( $args );
				} else {
					$input_args                      = $args;
					$input_args['allow_empty_value'] = $allow_empty_value;
					$string_id                       = $this->handle_db_error_and_resave_string( $input_args );
				}
			}

			icl_update_string_status( $string_id );

			$key          = md5( $domain . $name . $context );
			$cached_value = array(
				'id'    => $string_id,
				'value' => $value,
			);

			$this->get_domain_cache( $domain )->set( $key, $cached_value );
		} else {
			$string_id = 0;
		}

		return $string_id;
	}

	/**
	 * @param array $args
	 *
	 * @return int
	 */
	private function handle_db_error_and_resave_string( array $args ) {
		$repair_schema = new WPML_ST_Repair_Strings_Schema( wpml_get_admin_notices(), $args, $this->wpdb->last_error );

		if ( false !== strpos( $this->wpdb->last_error, 'translation_priority' ) ) {
			$repair_schema->set_command( new WPML_ST_Upgrade_DB_Strings_Add_Translation_Priority_Field( $this->wpdb ) );
		}

		if ( $repair_schema->run() ) {
			$string_id = $this->save_string(
				$args['value'],
				$args['allow_empty_value'],
				$args['language'],
				$args['context'],
				$args['gettext_context'],
				$args['name']
			);
		} else {
			$string_id                = 0;
			$this->block_save_strings = true;
		}

		return $string_id;
	}

	/**
	 * @param array $args
	 *
	 * @return int
	 */
	private function get_string_id_registered_in_concurrent_request( array $args ) {
		return (int) $this->wpdb->get_var(
			$this->wpdb->prepare(
				"SELECT id FROM {$this->wpdb->prefix}icl_strings WHERE domain_name_context_md5 = %s",
				md5( $args['context'] . $args['name'] . $args['gettext_context'] )
			)
		);
	}

	/**
	 * @param string          $name
	 * @param string|string[] $context
	 */
	protected function initialize_current_string( $name, $context ) {
		list ( $this->domain, $this->gettext_context ) = wpml_st_extract_context_parameters( $context );

		list( $this->name, $this->domain ) = array_map(
			array(
				$this,
				'truncate_long_string',
			),
			array( $name, $this->domain )
		);

		$this->name_and_gettext_context = $this->name . $this->gettext_context;
		$this->key                      = md5( $this->domain . $this->name_and_gettext_context );
	}

	/**
	 * @param string          $name
	 * @param string|string[] $context
	 *
	 * @return array
	 */
	protected function truncate_name_and_context( $name, $context ) {
		if ( is_array( $context ) ) {
			$domain          = isset( $context['domain'] ) ? $context['domain'] : '';
			$gettext_context = isset( $context['context'] ) ? $context['context'] : '';
		} else {
			$domain          = $context;
			$gettext_context = '';
		}
		list( $name, $domain ) = array_map(
			array(
				$this,
				'truncate_long_string',
			),
			array( $name, $domain )
		);

		return array( $name . $gettext_context, $domain );
	}

	protected function key_by_name_and_context( $name, $context ) {

		return array(
			$this->domain,
			$this->gettext_context,
			md5( $this->domain . $this->name_and_gettext_context ),
		);
	}

	/**
	 * @return WPML_Autoregister_Save_Strings
	 */
	private function get_save_strings() {
		if ( null === $this->save_strings ) {
			$this->save_strings = new WPML_Autoregister_Save_Strings( $this->wpdb, $this->sitepress );
		}

		return $this->save_strings;
	}

	/**
	 * @param string $domain
	 *
	 * @return WPML_WP_Cache
	 */
	private function get_domain_cache( $domain ) {
		$found        = false;
		$domain_cache = $this->registered_string_cache->get( $domain, $found );

		if ( ! $found ) {
			$domain_cache = new WPML_WP_Cache( 'WPML_Register_String_Filter::' . $domain );
			$this->registered_string_cache->set( $domain, $domain_cache );
		}

		return $domain_cache;
	}
}
