<?php
/**
 * Html Element Class
 *
 * @package errigal
 * @since 1.2.0
 */

namespace Errigal\Shared;

defined( 'ABSPATH' ) || exit;

use Errigal\Constant\{
	Errigal_Html_Element_Hook as Html_Element_Hook,
	Errigal_Html_Option as Html_Option,
	Errigal_Html_Tag as Html_Tag,
};
use Errigal\Shared\{
	Errigal_Html_Builder_Base as Html_Builder,
	Errigal_Html_Schema as Html_Schema,
	Errigal_Utility as Utility,
	Errigal_Validator as Validator,
};
use Closure;

/**
 * This class is in charge of providing HTML element functions.
 *
 * @since 1.2.0
 */
class Errigal_Html_Element {
	/**
	 * HTML tag name.
	 *
	 * @since 1.2.0
	 *
	 * @var string
	 */
	protected $tag;

	/**
	 * Base void/self-closing HTML elements.
	 *
	 * @since 1.2.0
	 *
	 * @var string[]
	 */
	private const BASE_VOID_ELEMENTS = array(
		Html_Tag::AREA,
		Html_Tag::BASE,
		Html_Tag::BR,
		Html_Tag::COL,
		Html_Tag::EMBED,
		Html_Tag::HR,
		Html_Tag::IMG,
		Html_Tag::INPUT,
		Html_Tag::LINK,
		Html_Tag::META,
		Html_Tag::SOURCE,
		Html_Tag::TRACK,
		Html_Tag::WBR,
	);

	/**
	 * HTML attributes.
	 *
	 * @since 1.2.0
	 *
	 * @var array
	 */
	private $attributes;

	/**
	 * Allowed HTML elements and attributes.
	 *
	 * @since 1.2.0
	 *
	 * @var array
	 */
	private $allowed_html;

	/**
	 * Html builder.
	 *
	 * @since 1.2.0
	 *
	 * @var Html_Builder
	 */
	private $html_builder;

	/**
	 * Constructor.
	 *
	 * @since 1.2.0
	 *
	 * @param Html_Builder $html_builder Instance of Html builder.
	 * @param string       $tag          Tag name.
	 * @param array        $args {
	 *     Optional. Associative array of arguments for HTML element. Default empty array.
	 *
	 *     @type array  $attrs        Optional. HTML attributes. Default empty array.
	 *     @type array  $allowed_html Optional. Allowed HTML elements and attributes.
	 *                                Default Errigal_Html_Schema::get_allowed_html_post().
	 * }
	 */
	public function __construct( $html_builder, $tag, $args = array() ) {
		$args         = wp_parse_args(
			$args,
			array(
				Html_Option::ATTRS        => array(),
				Html_Option::ALLOWED_HTML => Html_Schema::get_allowed_html_post(),
			)
		);
		$attributes   = $args[ Html_Option::ATTRS ];
		$allowed_html = $args[ Html_Option::ALLOWED_HTML ];

		$this->html_builder = Html_Builder::is_html_builder( $html_builder )
			? $html_builder
			: null;
		$this->tag          = Validator::is_non_empty_string( $tag )
			? $tag
			: null;
		$this->attributes   = Validator::is_associative_array( $attributes )
			? $attributes
			: array();
		$this->allowed_html = Validator::is_non_empty_array( $allowed_html )
			? Utility::array_merge_deep(
				Html_Schema::get_allowed_html_post(),
				$allowed_html
			)
			: Html_Schema::get_allowed_html_post();
	}

	/**
	 * Gets element.
	 *
	 * @since 1.2.0
	 *
	 * @param string $content Optional. HTML content. Default empty string.
	 * @return string
	 */
	public function get_element( $content = '' ) {
		if ( ! Validator::is_non_empty_string( $this->tag ) ) {
			return '';
		}

		$element = $this->get_open_tag();

		if ( ! $this->is_void_elements() ) {
			$element .= is_string( $content )
				? $content
				: '';
			$element .= $this->get_close_tag();
		}

		return $this->kses( $element );
	}

	/**
	 * Renders element.
	 *
	 * @since 1.2.0
	 *
	 * @param Closure|string $content Optional. Callback to render HTML content or HTML content.
	 *                                          Default empty string.
	 * @return void
	 */
	public function render_element( $content = '' ) {
		if ( Validator::is_closure( $content ) ) {
			$this->render_open_tag();
			call_user_func(
				$content,
				$this->html_builder
			);
			$this->render_close_tag();

			return;
		}

		$content = is_string( $content )
			? $content
			: '';

		// phpcs: Element is escaped in Errigal_Html_Element::get_element().
		echo $this->get_element( $content ); // phpcs:ignore WordPress.Security.EscapeOutput
	}

	/**
	 * Gets open tag.
	 *
	 * @since 1.2.0
	 *
	 * @return string
	 */
	public function get_open_tag() {
		if ( ! Validator::is_non_empty_string( $this->tag ) ) {
			return '';
		}

		return $this->kses(
			sprintf(
				'<%1$s%2$s>',
				tag_escape( $this->tag ),
				$this->get_attributes()
			)
		);
	}

	/**
	 * Renders open tag.
	 *
	 * @since 1.2.0
	 *
	 * @return void
	 */
	public function render_open_tag() {
		// phpcs: Open tag is escaped in Errigal_Html_Element::get_open_tag().
		echo $this->get_open_tag(); // phpcs:ignore WordPress.Security.EscapeOutput
	}

	/**
	 * Gets close tag.
	 *
	 * @since 1.2.0
	 *
	 * @return string
	 */
	public function get_close_tag() {
		if (
			! Validator::is_non_empty_string( $this->tag ) ||
			$this->is_void_elements()
		) {
			return '';
		}

		return $this->kses(
			sprintf(
				'</%s>',
				tag_escape( $this->tag )
			)
		);
	}

	/**
	 * Renders close tag.
	 *
	 * @since 1.2.0
	 *
	 * @return void
	 */
	public function render_close_tag() {
		// phpcs: Close tag is escaped in Errigal_Html_Element::get_close_tag().
		echo $this->get_close_tag(); // phpcs:ignore WordPress.Security.EscapeOutput
	}

	/**
	 * Filters text content and strips out disallowed HTML.
	 *
	 * @since 1.2.0
	 *
	 * @param string $content Text content to filter.
	 * @return string
	 */
	protected function kses( $content ) {
		return wp_kses( $content, $this->allowed_html );
	}

	/**
	 * Gets attributes.
	 *
	 * @since 1.2.0
	 *
	 * @return string
	 */
	protected function get_attributes() {
		if ( ! Validator::is_associative_array( $this->attributes ) ) {
			return '';
		}

		$output = '';

		foreach ( $this->attributes as $name => $value ) {
			$output .= Utility::get_attr( $name, $value );
		}

		return $output;
	}

	/**
	 * Determines whether element is void element.
	 *
	 * @since 1.2.0
	 *
	 * @return bool
	 */
	private function is_void_elements() {
		/**
		 * Filters list of void/self-closing HTML tags.
		 *
		 * @since 1.2.0
		 *
		 * @param string[] $void_elements Void element tag names.
		 */
		$void_elements = apply_filters(
			Html_Element_Hook::HTML_VOID_ELEMENTS,
			self::BASE_VOID_ELEMENTS
		);

		return Validator::has_string_list_item(
			$void_elements,
			$this->tag
		);
	}
}
