<?php
/**
 * Settings element classes for dashboard components.
 *
 * @package DokanKits\Admin\Dashboard\Components
 * @since   3.0.0
 */

namespace DokanKits\Admin\Dashboard\Components;

use Exception;
use RuntimeException;

/**
 * Settings Element Class.
 *
 * Base class for all settings UI elements.
 *
 * @since 3.0.0
 * @package DokanKits\Admin\Dashboard\Components
 */
abstract class SettingsElement {

	/**
	 * ID of the features element.
	 *
	 * @since 3.0.0
	 * @var string $id ID.
	 */
	protected string $id = '';

	/**
	 * Title of the  features element.
	 *
	 * @since 3.0.0
	 * @var string $title Title.
	 */
	protected string $title = '';

	/**
	 * Description of the features element.
	 *
	 * @since 3.0.0
	 * @var string $description Description.
	 */
	protected string $description = '';

	/**
	 * The Icon class for the features element.
	 *
	 * @since 3.0.0
	 * @var string $icon Icon.
	 */
	protected string $icon = '';

	/**
	 * Settings Element Value.
	 *
	 * @since 3.0.0
	 * @var mixed $value Value.
	 */
	protected $value;

	/**
	 * Is the element support children?
	 *
	 * @since 3.0.0
	 * @var bool $support_children Has children.
	 */
	protected bool $support_children = true;

	/**
	 * Children Settings elements.
	 *
	 * @since 3.0.0
	 * @var SettingsElement[] $children Children Elements.
	 */
	protected array $children = array();

	/**
	 * The features dependencies.
	 *
	 * @since 3.0.0
	 * @var array $dependencies Dependencies.
	 */
	protected array $dependencies = array();

	/**
	 * Settings Type.
	 *
	 * @since 3.0.0
	 * @var string $type Settings Type.
	 */
	protected string $type = '';

	/**
	 * The key for generating dynamic hook.
	 *
	 * @since 3.0.0
	 * @var string $hook_key Hook Key.
	 */
	public string $hook_key = '';

	/**
	 * The key for generating dynamic Dependency.
	 *
	 * @since 3.0.0
	 * @var string $dependency_key Dependency Key.
	 */
	public string $dependency_key = '';

	/**
	 * Default value for the element.
	 *
	 * @since 3.0.0
	 * @var mixed $default Default value.
	 */
	protected $default;

	/**
	 * The constructor.
	 *
	 * @since 3.0.0
	 * @param string $id ID of the features.
	 */
	public function __construct( string $id ) {
		/**
		 * Action before settings element initialization
		 *
		 * @since 3.0.0
		 * @param string $id Element ID.
		 */
		do_action( 'dokan_kits_before_settings_element_init', $id );

		$this->id = $id;

		/**
		 * Action after settings element initialization
		 *
		 * @since 3.0.0
		 *
		 * @param SettingsElement $element Element instance.
		 * @param string          $id      Element ID.
		 */
		do_action( 'dokan_kits_after_settings_element_init', $this, $id );
	}

	/**
	 * Get the ID of the Settings element.
	 *
	 * @since 3.0.0
	 * @return string
	 */
	public function get_id(): string {
		/**
		 * Filter element ID
		 *
		 * @since 3.0.0
		 *
		 * @param string          $id      Element ID.
		 * @param SettingsElement $element Element instance.
		 */
		return apply_filters( 'dokan_kits_settings_element_id', $this->id, $this );
	}

	/**
	 * Set the ID of the Settings element.
	 *
	 * @since 3.0.0
	 *
	 * @param string $id ID.
	 *
	 * @return SettingsElement
	 */
	public function set_id( string $id ): SettingsElement {
		$this->id = $id;

		return $this;
	}

	/**
	 * Get the Type of the Settings element.
	 *
	 * @since 3.0.0
	 * @return string
	 */
	public function get_type(): string {
		/**
		 * Filter element type
		 *
		 * @since 3.0.0
		 *
		 * @param string          $type    Element type.
		 * @param SettingsElement $element Element instance.
		 */
		return apply_filters( 'dokan_kits_settings_element_type', $this->type, $this );
	}

	/**
	 * Get the Title of the Settings element.
	 *
	 * @since 3.0.0
	 * @return string
	 */
	public function get_title(): string {
		/**
		 * Filter element title
		 *
		 * @since 3.0.0
		 *
		 * @param string          $title   Element title.
		 * @param SettingsElement $element Element instance.
		 */
		return apply_filters( 'dokan_kits_settings_element_title', $this->title, $this );
	}

	/**
	 * Set the Title of the Settings element.
	 *
	 * @since 3.0.0
	 *
	 * @param string $title Title.
	 *
	 * @return SettingsElement
	 */
	public function set_title( string $title ): SettingsElement {
		/**
		 * Filter element title before setting
		 *
		 * @since 3.0.0
		 *
		 * @param string          $title    Element title.
		 * @param SettingsElement $element  Element instance.
		 */
		$this->title = apply_filters( 'dokan_kits_settings_element_set_title', $title, $this );

		return $this;
	}

	/**
	 * Get the Description of the Settings element.
	 *
	 * @since 3.0.0
	 * @return string
	 */
	public function get_description(): string {
		return $this->description;
	}

	/**
	 * Set the Description of the Settings element.
	 *
	 * @since 3.0.0
	 *
	 * @param string $description The description.
	 *
	 * @return SettingsElement
	 */
	public function set_description( string $description ): SettingsElement {
		$this->description = $description;

		return $this;
	}

	/**
	 * Get the icon of the Settings element.
	 *
	 * @since 3.0.0
	 * @return string
	 */
	public function get_icon(): string {
		return $this->icon;
	}

	/**
	 * Set the icon of the Settings element.
	 *
	 * @since 3.0.0
	 *
	 * @param string $icon Icon class.
	 *
	 * @return SettingsElement
	 */
	public function set_icon( string $icon ): SettingsElement {
		$this->icon = $icon;

		return $this;
	}

	/**
	 * Get Hook Key.
	 *
	 * @since 3.0.0
	 * @return string
	 */
	public function get_hook_key(): string {
		return $this->hook_key;
	}

	/**
	 * Set Hook key.
	 *
	 * @since 3.0.0
	 *
	 * @param string $hook_key Key.
	 *
	 * @return SettingsElement
	 */
	public function set_hook_key( string $hook_key ): SettingsElement {
		$this->hook_key = $hook_key;

		return $this;
	}

	/**
	 * Get Dependencies key.
	 *
	 * @since 3.0.0
	 * @return string
	 */
	public function get_dependency_key(): string {
		return $this->dependency_key;
	}

	/**
	 * Set Dependencies key.
	 *
	 * @since 3.0.0
	 *
	 * @param string $dependency_key The dependency_key.
	 *
	 * @return SettingsElement
	 */
	public function set_dependency_key( string $dependency_key ): SettingsElement {
		$this->dependency_key = $dependency_key;

		return $this;
	}

	/**
	 * Get the Value for the element.
	 *
	 * @since 3.0.0
	 * @return mixed
	 */
	public function get_value() {
		$value = $this->value;

		if ( ! isset( $value ) && method_exists( $this, 'get_default' ) ) {
			$value = $this->get_default();
		}

		return $this->sanitize_element( $value );
	}

	/**
	 * Set The element value.
	 *
	 * @since 3.0.0
	 *
	 * @param mixed $value The element value.
	 *
	 * @return SettingsElement
	 * @throws Exception If value is not valid.
	 */
	public function set_value( $value ): SettingsElement {
		$this->value = $this->sanitize_element( $value );

		if ( $this->is_support_children() ) {
			$children = array();
			foreach ( $this->get_children() as $child ) {
				if ( isset( $value[ $child->get_id() ] ) ) {
					$child->set_value( $value[ $child->get_id() ] );
				}
				$children[ $child->get_id() ] = $child;
			}
			$this->set_children( $children );
		}

		return $this;
	}

	/**
	 * Check is the features element support children.
	 *
	 * @since 3.0.0
	 * @return bool
	 */
	public function is_support_children(): bool {
		return $this->support_children;
	}

	/**
	 * Get the children of the features elements.
	 *
	 * @since 3.0.0
	 * @return SettingsElement[]
	 */
	public function get_children(): array {
		$children = array();

		/**
		 * An array containing the filtered list of child elements or objects.
		 * This variable is intended to store a subset of children
		 * after applying specific filtering criteria.
		 *
		 * @since 4.0.0
		 *
		 * @param array           $children
		 * @param SettingsElement $element
		 */
		$filtered_children = apply_filters( $this->get_hook_key() . '_children', $this->children, $this ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound

		foreach ( $filtered_children as $child ) {
			$child->set_hook_key( $this->get_hook_key() . '_' . $child->get_id() );
			$child->set_dependency_key( trim( $this->get_dependency_key() . '.' . $child->get_id(), '. ' ) );
			$children[ $child->get_id() ] = $child;
		}

		return $children;
	}

	/**
	 * Set Children.
	 *
	 * @since 3.0.0
	 *
	 * @param array $children Children.
	 *
	 * @return SettingsElement
	 * @throws RuntimeException If children are not attachable.
	 */
	public function set_children( array $children ): SettingsElement {
		if ( ! $this->is_support_children() ) {
			// translators: %s is Settings element type.
			throw new RuntimeException( sprintf( esc_html__( 'Settings %s Does not support adding any children.', 'dokan-kits' ), esc_html( $this->get_type() ) ) );
		}

		$this->children = $children;

		return $this;
	}

	/**
	 * Get element dependency array.
	 *
	 * @since 3.0.0
	 * @return array
	 */
	public function get_dependencies(): array {
		$dependency_key = $this->get_dependency_key();

		return array_map(
			static function ( $dependency ) use ( $dependency_key ) {
				$dependency['self'] = $dependency_key;
				return $dependency;
			},
			$this->dependencies
		);
	}

	/**
	 * Set Dependencies.
	 *
	 * @since 3.0.0
	 *
	 * @param array $dependencies Dependencies.
	 *
	 * @return SettingsElement
	 */
	public function set_dependencies( array $dependencies ): SettingsElement {
		$this->dependencies = $dependencies;

		return $this;
	}

	/**
	 * Add Dependencies to the SettingsElement.
	 *
	 * @since 3.0.0
	 *
	 * @param string $key        Dot (.) seperated key string.
	 * @param mixed  $value      Value for comparison.
	 * @param bool   $to_self    Value for comparison.
	 * @param string $attribute  Attributes for operation (Optional).
	 * @param string $effect     The effect of dependency (Optional).
	 * @param string $comparison Value comparison operator (Optional).
	 *
	 * @return SettingsElement
	 */
	public function add_dependency( string $key, $value, bool $to_self = true, string $attribute = 'display', string $effect = 'hide', string $comparison = '=' ): SettingsElement {
		$this->dependencies[] = array(
			'key'        => $key,
			'value'      => $value,
			'to_self'    => $to_self,
			'attribute'  => $attribute,
			'effect'     => $effect,
			'comparison' => $comparison,
		);

		return $this;
	}

	/**
	 * Add child element.
	 *
	 * @since 3.0.0
	 *
	 * @param SettingsElement $element Settings element.
	 *
	 * @return $this
	 * @throws RuntimeException If child element is not attachable.
	 */
	public function add( SettingsElement $element ): SettingsElement {
		if ( ! $this->is_support_children() ) {
			// translators: %s is Settings element type.
			throw new RuntimeException( sprintf( esc_html__( 'Settings %s Does not support adding any children.', 'dokan-kits' ), esc_html( $this->get_type() ) ) );
		}

		$this->children[ $element->get_id() ] = $element;

		return $this;
	}

	/**
	 * Detach any child element.
	 *
	 * @since 3.0.0
	 *
	 * @param SettingsElement $element Child Element.
	 *
	 * @return SettingsElement
	 * @throws RuntimeException If Element is not removable.
	 */
	public function remove( SettingsElement $element ): SettingsElement {
		if ( ! $this->is_support_children() ) {
			// translators: %s is Settings element type.
			throw new RuntimeException( sprintf( esc_html__( 'Settings %s Does not support removing any children.', 'dokan-kits' ), esc_html( $this->get_type() ) ) );
		}

		$this->children = array_filter(
			$this->children,
			static fn( $child ) => $child !== $element
		);
		return $this;
	}

	/**
	 * Validate the Data.
	 *
	 * @since 3.0.0
	 * @param mixed $data Data to store.
	 *
	 * @return bool
	 */
	public function validate( $data ): bool {
		$validity = $this->data_validation( $data );

		if ( $validity && $this->is_support_children() ) {
			foreach ( $this->get_children() as $child ) {
				if ( ! isset( $data[ $child->get_id() ] ) || ! $child->validate( $data[ $child->get_id() ] ) ) {
					$validity = false;
					break;
				}
			}
		}

		return $validity;
	}

	/**
	 * Populate The features array.
	 *
	 * @since 3.0.0
	 * @return array
	 */
	public function populate(): array {
		$children = array();
		if ( $this->is_support_children() ) {
			foreach ( $this->get_children() as $child ) {
				$children[] = $child->populate();
			}
		}

		$populated_data = array(
			'id'             => $this->get_id(),
			'type'           => $this->get_type(),
			'title'          => $this->get_title(),
			'icon'           => $this->get_icon(),
			'display'        => true, // to manage element display action from dependencies.
			'hook_key'       => $this->get_hook_key(),
			'children'       => $children,
			'description'    => $this->get_description(),
			'dependency_key' => $this->get_dependency_key(),
			'dependencies'   => $this->get_dependencies(),
		);

		/**
		 * Filters the populated data for a features element.
		 * This filter allows modification of the complete array of populated data
		 * before it's returned from the populate method. The data includes all element
		 * properties such as ID, type, title, children, dependencies, etc.
		 *
		 * @since 4.0.0
		 *
		 * @param array           $populated_data The array containing all element data.
		 * @param SettingsElement $element        The current features element instance.
		 *
		 * @return array Modified populated data.
		 */
		return apply_filters( $this->get_hook_key() . '_populate', $populated_data, $this ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
	}

	/**
	 * Sanitize the features element.
	 *
	 * @since 3.0.0
	 * @param mixed $data Data for sanitization.
	 *
	 * @return array|string
	 */
	public function sanitize( $data ) {
		$data = $this->sanitize_element( $data );

		if ( $this->is_support_children() ) {
			foreach ( $this->get_children() as $child ) {
				$child_id = $child->get_id();

				$data[ $child_id ] = $child->sanitize( $data[ $child_id ] );
			}
		}

		return $data;
	}

	/**
	 * Data Validation condition.
	 *
	 * @since 3.0.0
	 * @param mixed $data Data for validation.
	 *
	 * @return bool
	 */
	abstract public function data_validation( $data ): bool;

	/**
	 * Sanitize data for storage.
	 *
	 * @since 3.0.0
	 * @param mixed $data Data for sanitization.
	 *
	 * @return mixed
	 */
	abstract public function sanitize_element( $data );

	/**
	 * Escape Output for usage.
	 *
	 * @since 3.0.0
	 * @param mixed $data Data for sanitization.
	 *
	 * @return mixed
	 */
	abstract public function escape_element( $data );
}
