<?php

use \WPML\FP\Fns;
use \WPML\TM\Jobs\FieldId;
use \WPML\FP\Logic;
use \WPML\FP\Lst;
use \WPML\FP\Either;
use \WPML\LIB\WP\Post;
use WPML\Translation\TranslationElements\FieldCompression;
use function \WPML\FP\curryN;
use function \WPML\FP\pipe;
use function \WPML\FP\invoke;
use WPML\TM\Jobs\Utils;
use WPML\FP\Relation;
use WPML\FP\Obj;
use WPML\TM\Jobs\JobLog;

/**
 * Class WPML_Element_Translation_Package
 *
 * @package wpml-core
 */
class WPML_Element_Translation_Package extends WPML_Translation_Job_Helper {

	const PACKAGE_TYPE_EXTERNAL = 'external';
	const PACKAGE_TYPE_POST     = 'post';

	const POST_IS_ORIGINAL = 'original';
	const POST_IS_UNKNOWN  = 'unknown';

	/** @var WPML_WP_API $wp_api */
	private $wp_api;

	/**
	 * @var array Cached objects.
	 */
	private static $cache = [
		self::PACKAGE_TYPE_EXTERNAL => [],
		self::PACKAGE_TYPE_POST     => [],
	];

	/**
	 * The constructor.
	 *
	 * @param WPML_WP_API $wp_api An instance of the WP API.
	 */
	public function __construct( WPML_WP_API $wp_api = null ) {
		global $sitepress;
		if ( $wp_api ) {
			$this->wp_api = $wp_api;
		} else {
			$this->wp_api = $sitepress->get_wp_api();
		}
	}

	/**
	 * @param int    $originalId
	 * @param string $packageType
	 * @param bool   $isOriginal
	 *
	 * @return array<string,string|array<string,string>>|null
	 */
	private function getCached( $originalId, $packageType, $isOriginal = false ) {
		$originalPath = $isOriginal ? self::POST_IS_ORIGINAL : self::POST_IS_UNKNOWN;
		return Obj::pathOr( null, [ $packageType, $originalId, $originalPath ], self::$cache );
	}

	/**
	 * @param int                                       $originalId
	 * @param string                                    $packageType
	 * @param array<string,string|array<string,string>> $package
	 * @param bool                                      $isOriginal
	 */
	private function setCached( $originalId, $packageType, $package, $isOriginal = false ) {
		if ( ! in_array( $packageType, [ self::PACKAGE_TYPE_EXTERNAL, self::PACKAGE_TYPE_POST ], true ) ) {
			return;
		}
		$originalPath = $isOriginal ? self::POST_IS_ORIGINAL : self::POST_IS_UNKNOWN;
		self::$cache[ $packageType ][ $originalId ][ $originalPath ] = $package;
	}

	public function clearCache() {
		self::$cache = [
			self::PACKAGE_TYPE_EXTERNAL => [],
			self::PACKAGE_TYPE_POST     => [],
		];
	}

	/**
	 * Create translation package
	 *
	 * @param \WPML_Package|\WP_Post|int $post
	 * @param bool                       $isOriginal
	 *
	 * @return array<string,string|array<string,string>>
	 */
	public function create_translation_package( $post, $isOriginal = false ) {

		$package = array();
		$post    = is_numeric( $post ) ? get_post( $post ) : $post;
		if ( apply_filters( 'wpml_is_external', false, $post ) ) {
			/** @var stdClass $post */
			$original_id   = isset( $post->post_id ) ? $post->post_id : $post->ID;

			JobLog::add( 'Creating external translation package for post `' . $original_id . '`' );

			$type          = self::PACKAGE_TYPE_EXTERNAL;
			$cachedPackage = $this->getCached( $original_id, $type, $isOriginal );
			if ( null !== $cachedPackage ) {
				JobLog::add( 'Found cached package for post `' . $original_id . '`' );
				return $cachedPackage;
			}

			$post_contents = (array) $post->string_data;
			JobLog::add(
				'Creating post contents in translation package for post `' . $original_id . '`',
				$post_contents
			);

			if ( isset( $post->title ) ) {
				$package['title'] = apply_filters( 'wpml_tm_external_translation_job_title', $post->title, $original_id );
			}

			if ( empty( $package['title'] ) ) {
				$package['title'] = sprintf(
					/* translators: The placeholder will be replaced with a number (an ID) */
					__( 'External package ID: %d', 'wpml-translation-management' ),
					$original_id
				);
			}
		} else {
			$original_id   = $post->ID;

			JobLog::add( 'Creating translation package for post `' . $original_id . '`' );

			$type          = self::PACKAGE_TYPE_POST;
			$cachedPackage = $this->getCached( $original_id, $type, $isOriginal );
			if ( null !== $cachedPackage ) {
				JobLog::add( 'Found cached package for post `' . $original_id . '`' );
				return $cachedPackage;
			}

			$home_url         = get_home_url();
			$package['url']   = htmlentities( $home_url . '?' . ( 'page' === $post->post_type ? 'page_id' : 'p' ) . '=' . ( $post->ID ) );
			$package['title'] = $post->post_title;

			$post_contents = array(
				'title'   => $post->post_title,
				'body'    => $post->post_content,
				'excerpt' => $post->post_excerpt,
			);

			if ( wpml_get_setting_filter( false, 'translated_document_page_url' ) === 'translate' ) {
				$post_contents['URL'] = $post->post_name;
			}

			$custom_fields_to_translate = \WPML\TM\Settings\Repository::getCustomFieldsToTranslate();

			if ( ! empty( $custom_fields_to_translate ) ) {
				$package = $this->add_custom_field_contents(
					$package,
					$post,
					$custom_fields_to_translate,
					$this->get_tm_setting( array( 'custom_fields_encoding' ) )
				);
			}

			$post_contents = array_merge( $post_contents, $this->get_taxonomy_fields( $post ) );
			JobLog::add(
				'Creating post contents in translation package for post `' . $original_id . '`',
				$post_contents
			);
		}

		$package['contents']['original_id'] = array(
			'translate' => 0,
			'data'      => $original_id,
		);
		$package['type']                    = $type;

		$package['contents'] = $this->buildEntries( $package['contents'], $post_contents );

		$package = apply_filters( 'wpml_tm_translation_job_data', $package, $post, $isOriginal );
		$this->setCached( $original_id, $type, $package, $isOriginal );
		return $package;
	}

	/**
     * Action for translation package per lang.
	 *
	 * This action is also used by wpml/wpml Word to Translate count.
	 *
	 * @param mixed $element For a post translation it should be a \WP_Post object.
	 *
	 * @return void
	 */
	public function do_action_before_creating_translation_package( $element ) {
		if ( $element instanceof \WP_Post ) {
			/**
			 * Registers strings coming from page builder shortcodes
			 *
			 * @param  \WP_Post $post
			 *
			 * @since 4.3.16
			 */
			do_action( 'wpml_pb_register_all_strings_for_translation', $element );
		}
	}

	/**
     * Filter for translation package per lang.
	 *
	 * This filter is also used by wpml/wpml Word to Translate count.
	 *
	 * @param array  $package
	 * @param mixed  $element For a post translation it should be a \WP_Post object.
	 * @param string $lang
	 *
	 * @return array
	 */
	public function filter_translation_package_for_lang( $package, $element, $lang ) {
		/**
		 * Filter translation package before creating the translation job.
		 *
		 * @param array|false $translation_package
		 * @param \WP_Post    $post
		 * @param string      $targetLang
		 *
		 * @since 4.5.12
		 */
		$package = apply_filters( 'wpml_translation_package_by_language', $package, $element, $lang );

		return $package;
	}

	private function buildEntries( $contents, $entries, $parentKey = '' ) {
		foreach ( $entries as $key => $entry ) {
			$fullKey = $parentKey ? $parentKey . '_' . $key : $key;

			if ( is_array( $entry ) ) {
				$contents = $this->buildEntries( $contents, $entry, $fullKey );
			} else {
				$contents[ $fullKey ] = [
					'translate' => 1,
					'data'      => base64_encode( $entry ),
					'format'    => 'base64',
				];
			}
		}

		return $contents;
	}

	/**
	 * @param array $translation_package
	 * @param int   $job_id
	 * @param array $prev_translation
	 * @param bool  $addJobLogs
	 */
	public function save_package_to_job( array $translation_package, $job_id, $prev_translation, $addJobLogs = false ) {
		global $wpdb;

		$show = $wpdb->hide_errors();

		$inserts = [];

		foreach ( $translation_package['contents'] as $field => $value ) {
			$job_translate = array(
				'job_id'                => $job_id,
				'content_id'            => 0,
				'field_type'            => $field,
				'field_wrap_tag'        => isset( $value['wrap_tag'] ) ? $value['wrap_tag'] : '',
				'field_format'          => isset( $value['format'] ) ? $value['format'] : '',
				'field_translate'       => $value['translate'],
				'field_data'            => $value['data'],
				'field_data_translated' => '',
				'field_finished'        => 0,
			);

			if ( array_key_exists( $field, $prev_translation ) ) {
				$job_translate['field_data_translated'] = $prev_translation[ $field ]->get_translation();
				$job_translate['field_finished']        = $prev_translation[ $field ]->is_finished( $value['data'] );
			}

			$job_translate = $this->filter_non_translatable_fields( $job_translate );

			$inserts[] = $wpdb->prepare(
				'( %d, %d, %s, %s, %s, %d, %s, %s, %d )',
				$job_translate['job_id'],
				$job_translate['content_id'],
				$job_translate['field_type'],
				$job_translate['field_wrap_tag'],
				$job_translate['field_format'],
				$job_translate['field_translate'],
        $job_translate['field_format'] === 'base64' ?
          FieldCompression::compressAndTrack( $job_translate['field_data'], true, $job_translate['job_id'] ) :
          $job_translate['field_data'],
        $job_translate['field_format'] === 'base64' ?
          FieldCompression::compressAndTrack( $job_translate['field_data_translated'], true, $job_translate['job_id'] ) :
          $job_translate['field_data_translated'],
				$job_translate['field_finished']
			);
		}

		if ( $inserts ) {
			$wpdb->query(
				'INSERT INTO ' . $wpdb->prefix . 'icl_translate ' .
				'(job_id, content_id, field_type, field_wrap_tag, field_format, field_translate, field_data, field_data_translated, field_finished) ' .
				'VALUES ' . implode( ', ', $inserts )
			);
			if ( $addJobLogs ) {
				JobLog::add( 'New icl_translate records created', $inserts );
			}
		}

		$wpdb->show_errors( $show );
	}

	/**
	 * @param array $job_translate
	 *
	 * @return mixed|void
	 */
	private function filter_non_translatable_fields( $job_translate ) {

		if ( $job_translate['field_translate'] ) {
			$data = $job_translate['field_data'];
			if ( 'base64' === $job_translate['field_format'] ) {
				// phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
				$data = base64_decode( $data );
			}
			$is_translatable = ! WPML_String_Functions::is_not_translatable( $data ) && apply_filters( 'wpml_translation_job_post_meta_value_translated', 1, $job_translate['field_type'] );

			/**
			 * Filters whether a translation job field is translatable.
			 *
			 * @param bool  $is_translatable Whether the field is translatable. Default value depends on previous logic.
			 * @param array $job_translate   The WPML translation job object.
			 * @param array $data            Additional data related to the translation job.
			 *
			 * @return bool
			 */
			$is_translatable = (bool) apply_filters( 'wpml_tm_job_field_is_translatable', $is_translatable, $job_translate, $data );
			if ( ! $is_translatable ) {
				$job_translate['field_translate']       = 0;
				$job_translate['field_data_translated'] = $job_translate['field_data'];
				$job_translate['field_finished']        = 1;
			}
		}

		return $job_translate;
	}

	/**
	 * @param object $job
	 * @param int    $post_id
	 * @param array  $fields
	 */
	public function save_job_custom_fields( $job, $post_id, $fields ) {
		$decode_translation = function ( $translation ) {
			// always decode html entities  eg decode &amp; to &.
			return html_entity_decode( str_replace( '&#0A;', "\n", $translation ) );
		};

		$get_field_id = function( $field_name, $el_data ) {
			if (
				strpos( $el_data->field_data, (string) $field_name ) === 0
				&& 1 === preg_match( '/field-(.*?)-name/U', $el_data->field_type, $match )
				&& 1 === preg_match( '/field-' . $field_name . '-[0-9].*?-name/', $el_data->field_type )
			) {
				return $match[1];
			}
			return null;
		};

		$field_names = [];

		foreach ( $fields as $field_name => $val ) {
			if ( '' === (string) $field_name ) {
				continue;
			}

			// find it in the translation.
			foreach ( $job->elements as $el_data ) {
				$field_id_string = $get_field_id( $field_name, $el_data );
				if ( $field_id_string ) {
					$field_names[ $field_name ] = isset( $field_names[ $field_name ] )
						? $field_names[ $field_name ] : [];

					$field_translation = false;
					foreach ( $job->elements as $v ) {
						if ( 'field-' . $field_id_string === $v->field_type ) {
							$field_translation = $this->decode_field_data(
								$v->field_data_translated,
								$v->field_format
							);
						}
						if ( 'field-' . $field_id_string . '-type' === $v->field_type ) {
							$field_type = $v->field_data;
							break;
						}
					}
					if ( false !== $field_translation && isset( $field_type ) && 'custom_field' === $field_type ) {
						$field_id_string = $this->remove_field_name_from_start( $field_name, $field_id_string );
						$meta_keys       = wpml_collect( explode( '-', $field_id_string ) )
							->map( [ 'WPML_TM_Field_Type_Encoding', 'decode_hyphen' ] )
							->prepend( $field_name )
							->toArray();
						$field_names     = Utils::insertUnderKeys( $meta_keys, $field_names, $decode_translation( $field_translation ) );
					}
				}
			}
		}

		$this->save_custom_field_values( $field_names, $post_id, $job->original_doc_id );
	}

	/**
	 * Remove the field from the start of the string.
	 *
	 * @param string $field_name The field to remove.
	 * @param string $field_id_string The full field identifier.
	 * @return string
	 */
	private function remove_field_name_from_start( $field_name, $field_id_string ) {
		return preg_replace( '#' . $field_name . '-?#', '', $field_id_string, 1 );
	}

	/**
	 * @param array $fields_in_job
	 * @param int   $post_id
	 * @param int   $original_post_id
	 */
	private function save_custom_field_values( $fields_in_job, $post_id, $original_post_id ) {
		$encodings = $this->get_tm_setting( array( 'custom_fields_encoding' ) );
		foreach ( $fields_in_job as $name => $contents ) {
			$this->wp_api->delete_post_meta( $post_id, $name );

			$contents = (array) $contents;
			$single   = count( $contents ) === 1;
			$encoding = isset( $encodings[ $name ] ) ? $encodings[ $name ] : '';

			foreach ( $contents as $value ) {

				$value = self::preserve_numerics( $value, $name, $original_post_id, $single );
				$value = $encoding ? WPML_Encoding::encode( $value, $encoding ) : $value;
				$value = apply_filters( 'wpml_encode_custom_field', $value, $name );
				$value = $this->prevent_strip_slash_on_json( $value, $encoding );

				$this->wp_api->add_post_meta( $post_id, $name, $value, $single );
			}
		}
	}

	/**
	 * The core function `add_post_meta` always performs
	 * a `stripslashes_deep` on the value. We need to escape
	 * once more before to call the function.
	 *
	 * @param string $value
	 * @param string $encoding
	 *
	 * @return string
	 */
	private function prevent_strip_slash_on_json( $value, $encoding ) {
		if ( in_array( 'json', explode( ',', $encoding ), true ) ) {
			$value = wp_slash( $value );
		}

		return $value;
	}

	/**
	 * @param array  $package
	 * @param object $post
	 * @param array  $fields_to_translate
	 * @param array  $fields_encoding
	 *
	 * @return array
	 */
	private function add_custom_field_contents( $package, $post, $fields_to_translate, $fields_encoding ) {
		foreach ( $fields_to_translate as $key ) {
			$encoding             = isset( $fields_encoding[ $key ] ) ? $fields_encoding[ $key ] : '';
			$custom_fields_values = array_values( array_filter( get_post_meta( $post->ID, $key ) ) );
			foreach ( $custom_fields_values as $index => $custom_field_val ) {
				$custom_field_val = apply_filters( 'wpml_decode_custom_field', $custom_field_val, $key );
				$package          = $this->add_single_field_content( $package, $key, array( $index ), $custom_field_val, $encoding );
			}
		}

		return $package;
	}

	/**
	 * For array valued custom fields cf is given in the form field-{$field_name}-join('-', $indicies)
	 *
	 * @param array                 $package
	 * @param string                $key
	 * @param array                 $custom_field_index
	 * @param array|stdClass|string $custom_field_val
	 * @param string                $encoding
	 *
	 * @return array
	 */
	private function add_single_field_content( $package, $key, $custom_field_index, $custom_field_val, $encoding ) {
		if ( $encoding && is_scalar( $custom_field_val ) ) {
			$custom_field_val = WPML_Encoding::decode( $custom_field_val, $encoding );
			$encoding         = '';
		}
		if ( is_scalar( $custom_field_val ) ) {
			list( $cf, $key_index ) = WPML_TM_Field_Type_Encoding::encode( $key, $custom_field_index );
			// phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
			$package['contents'][ $cf ]           = array(
				'translate' => 1,
				'data'      => base64_encode( (string) $custom_field_val ),
				'format'    => 'base64',
			);
			$package['contents'][ $cf . '-name' ] = array(
				'translate' => 0,
				'data'      => $key_index,
			);
			$package['contents'][ $cf . '-type' ] = array(
				'translate' => 0,
				'data'      => 'custom_field',
			);

		} else {
			foreach ( (array) $custom_field_val as $ind => $value ) {
				$package = $this->add_single_field_content(
					$package,
					$key,
					array_merge( $custom_field_index, array( $ind ) ),
					$value,
					$encoding
				);
			}
		}

		return $package;
	}

	/**
	 * Ensure that any numerics are preserved in the given value. eg any string like '10'
	 * will be converted to an integer if the corresponding original value was an integer.
	 *
	 * @param mixed      $value
	 * @param string     $name
	 * @param string|int $original_post_id
	 * @param bool       $single
	 *
	 * @return mixed
	 */
	public static function preserve_numerics( $value, $name, $original_post_id, $single ) {
		$get_original = function () use ( $original_post_id, $name, $single ) {
			$meta = get_post_meta( (int) $original_post_id, $name, $single );
			return apply_filters( 'wpml_decode_custom_field', $meta, $name );
		};
		if ( is_numeric( $value ) && is_int( $get_original() ) ) {
			$value = intval( $value );
		} elseif ( is_array( $value ) ) {
			$value = self::preserve_numerics_recursive( $get_original(), $value );
		}

		return $value;
	}

	/**
	 * Ensure that any numerics are preserved in the given value. eg any string like '10'
	 * will be converted to an integer if the corresponding original value was an integer.
	 *
	 * @param mixed $original
	 * @param mixed $value
	 *
	 * @return mixed
	 */
	private static function preserve_numerics_recursive( $original, $value ) {
		if ( is_array( $original ) ) {
			foreach ( $original as $key => $data ) {
				if ( isset( $value[ $key ] ) ) {
					if ( is_array( $data ) ) {
						$value[ $key ] = self::preserve_numerics_recursive( $data, $value[ $key ] );
					} elseif ( is_int( $data ) && is_numeric( $value[ $key ] ) ) {
						$value[ $key ] = intval( $value[ $key ] );
					}
				}
			}
		}

		return $value;
	}

	private function get_taxonomy_fields( $post ) {
		global $sitepress;

		$termMetaKeysToTranslate = self::getTermMetaKeysToTranslate();

		// $getTermFields :: WP_Term → [[fieldId, fieldVal]]
		$getTermFields = function ( $term ) {
			return [
				[ FieldId::forTerm( $term->term_taxonomy_id ), $term->name ],
				[ FieldId::forTermDescription( $term->term_taxonomy_id ), $term->description ],
			];
		};

		// $getTermMetaFields :: [metakeys] → WP_Term → [[fieldId, fieldVal]]
		$getTermMetaFields = curryN(
			2,
			function ( $termMetaKeysToTranslate, $term ) {

				// $getMeta :: int → string → object
				$getMeta = curryN(
					3,
					function ( $termId, $termTaxonomyId, $key ) {
						return (object) [
							'id'   => $termTaxonomyId,
							'key'  => $key,
							'meta' => get_term_meta( $termId, $key ),
						];
					}
				);

				// $hasMeta :: object → bool
				$hasMeta = function ( $termData ) {
					return isset( $termData->meta[0] );
				};

				// $makeField :: object → [fieldId, $fieldVal]
				$makeField = function ( $termData ) {
					return [ FieldId::forTermMeta( $termData->id, $termData->key ), $termData->meta[0] ];
				};

				// $get :: [metakeys] → [[fieldId, $fieldVal]]
				$get = pipe(
					Fns::map( $getMeta( $term->term_id, $term->term_taxonomy_id ) ),
					Fns::filter( $hasMeta ),
					Fns::map( $makeField )
				);

				return $get( $termMetaKeysToTranslate );
			}
		);

		// $getAll :: [WP_Term] → [[fieldId, fieldVal]]
		$getAll = Fns::converge( Lst::concat(), [ $getTermFields, $getTermMetaFields( $termMetaKeysToTranslate ) ] );

		return wpml_collect( $sitepress->get_translatable_taxonomies( false, $post->post_type ) ) // [taxonomies]
			->map( Post::getTerms( $post->ID ) ) // [Either false|WP_Error [WP_Term]]
			->filter( Fns::isRight() ) // [Right[WP_Term]]
			->map( invoke( 'get' ) ) // [[WP_Term]]
			->flatten() // [WP_Term]
			->map( $getAll ) // [[fieldId, fieldVal]]
			->mapWithKeys( Lst::fromPairs() ) // [fieldId => fieldVal]
			->toArray();
	}

	public static function getTermMetaKeysToTranslate() {
		$fieldTranslation = new WPML_Custom_Field_Setting_Factory( self::get_core_translation_management() );

		$settingsFactory      = self::get_core_translation_management()->settings_factory();

		$translatableMetaKeys = pipe(
			[ $settingsFactory, 'term_meta_setting' ],
			invoke( 'status' ),
			Relation::equals( WPML_TRANSLATE_CUSTOM_FIELD )
		);

		return wpml_collect( $fieldTranslation->get_term_meta_keys() )
			->filter( $translatableMetaKeys )
			->values()
			->toArray();
	}
}
