<?php
/**
 * Markdown to Gutenberg converter service.
 *
 * @package SmartyPress_AI_Engine
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Converts generated Markdown/HTML content into Gutenberg block markup.
 */
class SmartyPress_AI_Content_Generator_Markdown_Block_Converter {

	/**
	 * Convert Markdown-like content into Gutenberg blocks.
	 *
	 * @param string $content Raw generated content.
	 * @return string Gutenberg block content.
	 */
	public function markdown_to_gutenberg_content( $content ) {
		$content = trim( (string) $content );

		if ( '' === $content ) {
			return '';
		}

		if ( false !== strpos( $content, '<!-- wp:' ) ) {
			return $content;
		}

		$blocks      = array();
		$paragraphs  = preg_split( "/\n\s*\n/", $content );
		$paragraphs  = is_array( $paragraphs ) ? $paragraphs : array();
		$list_buffer = array();

		foreach ( $paragraphs as $paragraph ) {
			$paragraph = trim( $paragraph );

			if ( '' === $paragraph ) {
				continue;
			}

			if ( preg_match( '/^(#{2,6})\s+(.+)$/', $paragraph, $matches ) ) {
				$this->flush_list_buffer( $blocks, $list_buffer );

				$level    = strlen( $matches[1] );
				$heading  = $this->convert_inline_markdown( trim( $matches[2] ) );
				$blocks[] = sprintf(
					'<!-- wp:heading {"level":%1$d} --><h%1$d class="wp-block-heading">%2$s</h%1$d><!-- /wp:heading -->',
					$level,
					$heading
				);
				continue;
			}

			if ( $this->is_list_paragraph( $paragraph ) ) {
				$list_buffer[] = $paragraph;
				continue;
			}

			$this->flush_list_buffer( $blocks, $list_buffer );

			$blocks[] = '<!-- wp:paragraph --><p>' . $this->convert_inline_markdown( $paragraph ) . '</p><!-- /wp:paragraph -->';
		}

		$this->flush_list_buffer( $blocks, $list_buffer );

		return implode( "\n\n", $blocks );
	}

	/**
	 * Convert HTML to Gutenberg blocks format.
	 *
	 * @param string $html HTML content.
	 * @return string Gutenberg blocks format content.
	 */
	public function html_to_gutenberg_blocks( $html ) {
		if ( empty( $html ) ) {
			return $html;
		}

		$block_map = array(
			'h1'         => 'core/heading',
			'h2'         => 'core/heading',
			'h3'         => 'core/heading',
			'h4'         => 'core/heading',
			'h5'         => 'core/heading',
			'h6'         => 'core/heading',
			'p'          => 'core/paragraph',
			'ul'         => 'core/list',
			'ol'         => 'core/list',
			'blockquote' => 'core/quote',
			'pre'        => 'core/code',
		);

		$dom = new DOMDocument();
		libxml_use_internal_errors( true );
		$html = mb_convert_encoding( $html, 'HTML-ENTITIES', 'UTF-8' );
		$dom->loadHTML( '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body>' . $html . '</body></html>', LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD );
		libxml_clear_errors();
		libxml_use_internal_errors( false );

		$body = $dom->getElementsByTagName( 'body' )->item( 0 );
		if ( ! $body ) {
			return $html;
		}

		$blocks      = array();
		$child_nodes = $body->childNodes;

		foreach ( $child_nodes as $node ) {
			if ( XML_ELEMENT_NODE === $node->nodeType ) {
				$tag_name = strtolower( $node->nodeName );

				if ( isset( $block_map[ $tag_name ] ) ) {
					$block_name       = $block_map[ $tag_name ];
					$block_content    = $dom->saveHTML( $node );
					$block_attributes = array();

					if ( in_array( $tag_name, array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ), true ) ) {
						$level                     = (int) substr( $tag_name, 1 );
						$block_attributes['level'] = $level;
						$anchor                    = $node->getAttribute( 'id' );

						if ( '' !== $anchor ) {
							$block_attributes['anchor'] = sanitize_title( $anchor );
						}

						$block_content = sprintf(
							'<h%1$d class="wp-block-heading"%2$s>%3$s</h%1$d>',
							$level,
							'' !== $anchor ? ' id="' . esc_attr( sanitize_title( $anchor ) ) . '"' : '',
							wp_kses_post( $this->get_inner_html( $node, $dom ) )
						);
					}

					if ( 'blockquote' === $tag_name ) {
						$blocks[] = $this->render_quote_block( $node, $dom );
					} elseif ( in_array( $tag_name, array( 'ul', 'ol' ), true ) ) {
						$blocks[] = $this->render_list_block( $node, $dom, 'ol' === $tag_name );
					} else {
						$blocks[] = $this->create_gutenberg_block( $block_name, $block_attributes, $block_content );
					}
				} elseif ( 'div' !== $tag_name && 'br' !== $tag_name ) {
					$block_content = $dom->saveHTML( $node );
					$blocks[]      = $this->create_gutenberg_block( 'core/paragraph', array(), $block_content );
				}
			} elseif ( XML_TEXT_NODE === $node->nodeType ) {
				$text = trim( $node->textContent );

				if ( ! empty( $text ) ) {
					$block_content = '<p>' . esc_html( $text ) . '</p>';
					$blocks[]      = $this->create_gutenberg_block( 'core/paragraph', array(), $block_content );
				}
			}
		}

		return implode( "\n\n", $blocks );
	}

	/**
	 * Render a Parsedown blockquote as valid Gutenberg quote markup.
	 *
	 * @param DOMElement  $node Quote node.
	 * @param DOMDocument $dom  DOM document.
	 * @return string
	 */
	private function render_quote_block( DOMElement $node, DOMDocument $dom ) {
		$paragraphs = array();

		foreach ( $node->childNodes as $child ) {
			if ( XML_ELEMENT_NODE === $child->nodeType && 'p' === strtolower( $child->nodeName ) ) {
				$paragraphs[] = $this->create_gutenberg_block(
					'core/paragraph',
					array(),
					'<p>' . wp_kses_post( $this->get_inner_html( $child, $dom ) ) . '</p>'
				);
			}
		}

		if ( empty( $paragraphs ) ) {
			$text = trim( $node->textContent );
			if ( '' === $text ) {
				return '';
			}

			$paragraphs[] = $this->create_gutenberg_block(
				'core/paragraph',
				array(),
				'<p>' . esc_html( $text ) . '</p>'
			);
		}

		return $this->create_gutenberg_block(
			'core/quote',
			array(),
			'<blockquote class="wp-block-quote">' . implode( '', $paragraphs ) . '</blockquote>'
		);
	}

	/**
	 * Render a Markdown list as valid Gutenberg list markup.
	 *
	 * @param DOMElement  $node List node.
	 * @param DOMDocument $dom DOM document.
	 * @param bool        $ordered Whether the list is ordered.
	 * @return string
	 */
	private function render_list_block( DOMElement $node, DOMDocument $dom, $ordered = false ) {
		$items = array();

		foreach ( $node->childNodes as $child ) {
			if ( XML_ELEMENT_NODE !== $child->nodeType || 'li' !== strtolower( $child->nodeName ) ) {
				continue;
			}

			$items[] = $this->create_gutenberg_block(
				'core/list-item',
				array(),
				'<li>' . wp_kses_post( $this->get_inner_html( $child, $dom ) ) . '</li>'
			);
		}

		if ( empty( $items ) ) {
			return '';
		}

		$tag        = $ordered ? 'ol' : 'ul';
		$attributes = $ordered ? array( 'ordered' => true ) : array();

		return $this->create_gutenberg_block(
			'core/list',
			$attributes,
			'<' . $tag . ' class="wp-block-list">' . implode( '', $items ) . '</' . $tag . '>'
		);
	}

	/**
	 * Get inner HTML for a DOM node.
	 *
	 * @param DOMNode     $node Node.
	 * @param DOMDocument $dom  DOM document.
	 * @return string
	 */
	private function get_inner_html( DOMNode $node, DOMDocument $dom ) {
		$html = '';

		foreach ( $node->childNodes as $child ) {
			$html .= $dom->saveHTML( $child );
		}

		return $html;
	}

	/**
	 * Create Gutenberg block with comment delimiters.
	 *
	 * @param string $block_name Block name.
	 * @param array  $attributes Block attributes.
	 * @param string $content Block content.
	 * @return string Gutenberg block format string.
	 */
	public function create_gutenberg_block( $block_name, $attributes = array(), $content = '' ) {
		if ( function_exists( 'get_comment_delimited_block_content' ) ) {
			return get_comment_delimited_block_content( $block_name, $attributes, $content );
		}

		$serialized_block_name = str_replace( 'core/', '', $block_name );

		if ( function_exists( 'serialize_block_attributes' ) ) {
			$serialized_attributes = empty( $attributes ) ? '' : serialize_block_attributes( $attributes ) . ' ';
		} else {
			$serialized_attributes = '';

			if ( ! empty( $attributes ) ) {
				$serialized_attributes = wp_json_encode( $attributes, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
				$serialized_attributes = preg_replace( '/--/', '\\u002d\\u002d', $serialized_attributes );
				$serialized_attributes = preg_replace( '/</', '\\u003c', $serialized_attributes );
				$serialized_attributes = preg_replace( '/>/', '\\u003e', $serialized_attributes );
				$serialized_attributes = preg_replace( '/&/', '\\u0026', $serialized_attributes );
				$serialized_attributes .= ' ';
			}
		}

		$content = trim( $content );

		if ( empty( $content ) ) {
			return sprintf( '<!-- wp:%s %s/-->', $serialized_block_name, $serialized_attributes );
		}

		return sprintf(
			'<!-- wp:%s %s-->%s<!-- /wp:%s -->',
			$serialized_block_name,
			$serialized_attributes,
			$content,
			$serialized_block_name
		);
	}

	/**
	 * Flush buffered markdown list paragraphs as one Gutenberg list block.
	 *
	 * @param array $blocks Generated blocks.
	 * @param array $list_buffer Buffered list paragraphs.
	 * @return void
	 */
	private function flush_list_buffer( &$blocks, &$list_buffer ) {
		if ( empty( $list_buffer ) ) {
			return;
		}

		$items = array();

		foreach ( $list_buffer as $paragraph ) {
			$lines = preg_split( '/\r\n|\r|\n/', $paragraph );
			$lines = is_array( $lines ) ? $lines : array();

			foreach ( $lines as $line ) {
				if ( preg_match( '/^\s*[-*]\s+(.+)$/', $line, $matches ) ) {
					$items[] = $this->create_gutenberg_block(
						'core/list-item',
						array(),
						'<li>' . $this->convert_inline_markdown( trim( $matches[1] ) ) . '</li>'
					);
				}
			}
		}

		if ( ! empty( $items ) ) {
			$blocks[] = $this->create_gutenberg_block(
				'core/list',
				array(),
				'<ul class="wp-block-list">' . implode( '', $items ) . '</ul>'
			);
		}

		$list_buffer = array();
	}

	/**
	 * Check whether a paragraph contains markdown list items only.
	 *
	 * @param string $paragraph Paragraph.
	 * @return bool
	 */
	private function is_list_paragraph( $paragraph ) {
		$lines = preg_split( '/\r\n|\r|\n/', $paragraph );
		$lines = is_array( $lines ) ? array_filter( array_map( 'trim', $lines ) ) : array();

		if ( empty( $lines ) ) {
			return false;
		}

		foreach ( $lines as $line ) {
			if ( ! preg_match( '/^[-*]\s+.+$/', $line ) ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Convert small inline Markdown subset and keep safe HTML.
	 *
	 * @param string $text Text.
	 * @return string
	 */
	private function convert_inline_markdown( $text ) {
		$text = wp_kses_post( (string) $text );
		$text = preg_replace( '/\*\*(.*?)\*\*/s', '<strong>$1</strong>', $text );
		$text = preg_replace( '/\*(.*?)\*/s', '<em>$1</em>', $text );

		return $text;
	}
}
