<?php
/**
 * @package Polylang
 */

namespace WP_Syntex\Polylang\Options;

use WP_Error;
use WP_Term;
use WP_Syntex\Polylang\Options\Options;

defined( 'ABSPATH' ) || exit;

/**
 * Class defining a single option.
 *
 * @since 3.7
 *
 * @phpstan-type SchemaType 'string'|'null'|'number'|'integer'|'boolean'|'array'|'object'
 * @phpstan-type Schema array{
 *     type: SchemaType
 * }
 */
abstract class Abstract_Option {
	/**
	 * Option value.
	 *
	 * @var mixed
	 */
	private $value;

	/**
	 * Cached option JSON schema.
	 *
	 * @var array|null
	 *
	 * @phpstan-var Schema|null
	 */
	private $schema;

	/**
	 * Validation and sanitization errors.
	 *
	 * @var WP_Error
	 */
	protected $errors;

	/**
	 * Constructor.
	 *
	 * @since 3.7
	 *
	 * @param mixed $value Optional. Option value.
	 */
	public function __construct( $value = null ) {
		$this->errors = new WP_Error();

		if ( ! isset( $value ) ) {
			$this->value = $this->get_default();
			return;
		}

		$value = rest_sanitize_value_from_schema( $this->prepare( $value ), $this->get_data_structure(), static::key() );

		if ( ! is_wp_error( $value ) ) {
			$this->value = $value;
		} else {
			$this->value = $this->get_default();
		}
	}

	/**
	 * Returns option key.
	 *
	 * @since 3.7
	 *
	 * @return string
	 *
	 * @phpstan-return non-falsy-string
	 */
	abstract public static function key(): string;

	/**
	 * Sets option's value if valid, does nothing otherwise.
	 *
	 * @since 3.7
	 *
	 * @param mixed   $value   Value to set.
	 * @param Options $options All options.
	 * @return bool True if the value has been assigned. False in case of errors.
	 */
	public function set( $value, Options $options ): bool {
		$this->errors = new WP_Error(); // Reset errors.
		$value        = $this->prepare( $value );
		$is_valid     = rest_validate_value_from_schema( $value, $this->get_data_structure(), static::key() );

		if ( is_wp_error( $is_valid ) ) {
			// Blocking validation error.
			$this->errors->merge_from( $is_valid );
			return false;
		}

		$value = $this->sanitize( $value, $options );

		if ( is_wp_error( $value ) ) {
			// Blocking sanitization error.
			$this->errors->merge_from( $value );
			return false;
		}

		$this->value = $value;
		return true;
	}

	/**
	 * Returns option's value.
	 *
	 * @since 3.7
	 *
	 * @return mixed
	 */
	public function &get() {
		return $this->value;
	}

	/**
	 * Sets default option value.
	 *
	 * @since 3.7
	 *
	 * @return mixed The new value.
	 */
	public function reset() {
		$this->value = $this->get_default();
		return $this->value;
	}

	/**
	 * Returns JSON schema of the option.
	 *
	 * @since 3.7
	 *
	 * @return array The schema.
	 *
	 * @phpstan-return Schema
	 */
	public function get_schema(): array {
		if ( is_array( $this->schema ) ) {
			return $this->schema;
		}

		$this->schema = array_merge(
			array(
				'description' => $this->get_description(),
				'default'     => $this->get_default(),
			),
			$this->get_data_structure()
		);

		return $this->schema;
	}

	/**
	 * Returns non-blocking sanitization errors.
	 *
	 * @since 3.7
	 *
	 * @return WP_Error
	 */
	public function get_errors(): WP_Error {
		return $this->errors;
	}

	/**
	 * Prepares a value before validation.
	 *
	 * @since 3.7
	 *
	 * @param mixed $value Value to format.
	 * @return mixed
	 */
	protected function prepare( $value ) {
		return $value;
	}

	/**
	 * Sanitizes option's value, can be overridden for specific cases not handled by `rest_sanitize_value_from_schema()`.
	 * Can populate the `$errors` property with blocking and non-blocking errors: in case of non-blocking errors,
	 * the value is sanitized and can be stored.
	 *
	 * @since 3.7
	 *
	 * @param mixed   $value   Value to sanitize.
	 * @param Options $options All options.
	 * @return mixed The sanitized value. An instance of `WP_Error` in case of blocking error.
	 */
	protected function sanitize( $value, Options $options ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		return rest_sanitize_value_from_schema( $value, $this->get_data_structure(), static::key() );
	}

	/**
	 * Returns the default value.
	 *
	 * @since 3.7
	 *
	 * @return mixed
	 */
	abstract protected function get_default();

	/**
	 * Returns the JSON schema part specific to this option.
	 *
	 * @since 3.7
	 *
	 * @return array Partial schema.
	 *
	 * @phpstan-return array{type: SchemaType}
	 */
	abstract protected function get_data_structure(): array;

	/**
	 * Returns the description used in the JSON schema.
	 *
	 * @since 3.7
	 *
	 * @return string
	 */
	abstract protected function get_description(): string;

	/**
	 * Returns a list of language terms.
	 *
	 * @since 3.7
	 *
	 * @return array
	 *
	 * @phpstan-return list<WP_Term>
	 */
	protected function get_language_terms(): array {
		$language_terms = get_terms(
			array(
				'taxonomy'   => 'language',
				'hide_empty' => false,
			)
		);
		return is_array( $language_terms ) ? $language_terms : array();
	}

	/**
	 * Adds a non-blocking error warning about unknown language slugs.
	 *
	 * @since 3.7
	 *
	 * @param array $language_slugs List of language slugs.
	 * @return void
	 */
	protected function add_unknown_languages_warning( array $language_slugs ): void {
		if ( 1 === count( $language_slugs ) ) {
			/* translators: %s is a language slug. */
			$message = __( 'The language %s is unknown and has been discarded.', 'polylang' );
		} else {
			/* translators: %s is a list of language slugs. */
			$message = __( 'The languages %s are unknown and have been discarded.', 'polylang' );
		}

		$this->errors->add(
			sprintf( 'pll_unknown_%s_languages', static::key() ),
			sprintf(
				$message,
				wp_sprintf_l(
					'%l',
					array_map(
						function ( $slug ) {
							return "<code>{$slug}</code>";
						},
						$language_slugs
					)
				)
			),
			'warning'
		);
	}
}
