<?php
declare( strict_types = 1 );

namespace Wikimedia\Parsoid\Ext\Gallery;

use stdClass;
use Wikimedia\Parsoid\Core\MediaStructure;
use Wikimedia\Parsoid\DOM\DocumentFragment;
use Wikimedia\Parsoid\DOM\Element;
use Wikimedia\Parsoid\DOM\Text;
use Wikimedia\Parsoid\Ext\DOMDataUtils;
use Wikimedia\Parsoid\Ext\DOMUtils;
use Wikimedia\Parsoid\Ext\ExtensionModule;
use Wikimedia\Parsoid\Ext\ExtensionTagHandler;
use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI;
use Wikimedia\Parsoid\Utils\DOMCompat;
use Wikimedia\Parsoid\Utils\PHPUtils;

/**
 * Implements the php parser's `renderImageGallery` natively.
 *
 * Params to support (on the extension tag):
 * - showfilename
 * - caption
 * - mode
 * - widths
 * - heights
 * - perrow
 *
 * A proposed spec is at: https://phabricator.wikimedia.org/P2506
 */
class Gallery extends ExtensionTagHandler implements ExtensionModule {

	/** @inheritDoc */
	public function getConfig(): array {
		return [
			'name' => 'Gallery',
			'tags' => [
				[
					'name' => 'gallery',
					'handler' => self::class,
				]
			],
		];
	}

	/**
	 * Parse the gallery caption.
	 * @param ParsoidExtensionAPI $extApi
	 * @param array $extArgs
	 * @return ?DocumentFragment
	 */
	private function pCaption(
		ParsoidExtensionAPI $extApi, array $extArgs
	): ?DocumentFragment {
		return $extApi->extArgToDOM( $extArgs, 'caption' );
	}

	/**
	 * Parse a single line of the gallery.
	 * @param ParsoidExtensionAPI $extApi
	 * @param string $line
	 * @param int $lineStartOffset
	 * @param Opts $opts
	 * @return ParsedLine|null
	 */
	private static function pLine(
		ParsoidExtensionAPI $extApi, string $line, int $lineStartOffset,
		Opts $opts
	): ?ParsedLine {
		// Regexp from php's `renderImageGallery`
		if ( !preg_match( '/^([^|]+)(\|(?:.*))?$/D', $line, $matches ) ) {
			return null;
		}

		$titleStr = $matches[1];
		$imageOptStr = $matches[2] ?? '';

		// TODO: % indicates rawurldecode.

		$mode = Mode::byName( $opts->mode );

		$imageOpts = [
			"|{$mode->dimensions( $opts )}",
			[ $imageOptStr, $lineStartOffset + strlen( $titleStr ) ],
		];

		$thumb = $extApi->renderMedia(
			$titleStr, $imageOpts, $error,
			// Force block for an easier structure to manipulate, otherwise
			// we have to pull the caption out of the data-mw
			true
		);
		if ( !$thumb || DOMCompat::nodeName( $thumb ) !== 'figure' ) {
			return null;
		}

		$doc = $thumb->ownerDocument;
		$rdfaType = $thumb->getAttribute( 'typeof' ) ?? '';

		// Detach figcaption as well
		$figcaption = DOMCompat::querySelector( $thumb, 'figcaption' );
		if ( !$figcaption ) {
			$figcaption = $doc->createElement( 'figcaption' );
		} else {
			DOMCompat::remove( $figcaption );
		}

		if ( $opts->showfilename ) {
			// No need for error checking on this call since it was already
			// done in $extApi->renderMedia() above
			$title = $extApi->makeTitle(
				$titleStr,
				$extApi->getSiteConfig()->canonicalNamespaceId( 'file' )
			);
			$file = $title->getPrefixedDBKey();
			$galleryfilename = $doc->createElement( 'a' );
			$galleryfilename->setAttribute( 'href', $extApi->getTitleUri( $title ) );
			$galleryfilename->setAttribute( 'class', 'galleryfilename galleryfilename-truncate' );
			$galleryfilename->setAttribute( 'title', $file );
			$galleryfilename->appendChild( $doc->createTextNode( $file ) );
			$figcaption->insertBefore( $galleryfilename, $figcaption->firstChild );
		}

		$gallerytext = null;
		for ( $capChild = $figcaption->firstChild;
			 $capChild !== null;
			 $capChild = $capChild->nextSibling ) {
			if (
				$capChild instanceof Text &&
				preg_match( '/^\s*$/D', $capChild->nodeValue )
			) {
				// skip blank text nodes
				continue;
			}
			// Found a non-blank node!
			$gallerytext = $figcaption;
			break;
		}

		return new ParsedLine( $thumb, $gallerytext, $rdfaType );
	}

	/** @inheritDoc */
	public function sourceToDom(
		ParsoidExtensionAPI $extApi, string $content, array $args
	): DocumentFragment {
		$attrs = $extApi->extArgsToArray( $args );
		$opts = new Opts( $extApi, $attrs );

		$offset = $extApi->extTag->getOffsets()->innerStart();

		// Prepare the lines for processing
		$lines = explode( "\n", $content );
		$lines = array_map( static function ( $line ) use ( &$offset ) {
				$lineObj = [ 'line' => $line, 'offset' => $offset ];
				$offset += strlen( $line ) + 1; // For the nl
				return $lineObj;
		}, $lines );

		$caption = $opts->caption ? $this->pCaption( $extApi, $args ) : null;
		$lines = array_map( function ( $lineObj ) use ( $extApi, $opts ) {
			return $this->pLine(
				$extApi, $lineObj['line'], $lineObj['offset'], $opts
			);
		}, $lines );

		// Drop invalid lines like "References: 5."
		$lines = array_filter( $lines, static function ( $lineObj ) {
			return $lineObj !== null;
		} );

		$mode = Mode::byName( $opts->mode );
		$extApi->addModules( $mode->getModules() );
		$extApi->addModuleStyles( $mode->getModuleStyles() );
		return $mode->render( $extApi, $opts, $caption, $lines );
	}

	/**
	 * @param ParsoidExtensionAPI $extApi
	 * @param Element $node
	 * @return string
	 */
	private function contentHandler(
		ParsoidExtensionAPI $extApi, Element $node
	): string {
		$content = "\n";
		for ( $child = $node->firstChild; $child; $child = $child->nextSibling ) {
			switch ( $child->nodeType ) {
			case XML_ELEMENT_NODE:
				DOMUtils::assertElt( $child );
				// Ignore if it isn't a "gallerybox"
				if (
					DOMCompat::nodeName( $child ) !== 'li' ||
					$child->getAttribute( 'class' ) !== 'gallerybox'
				) {
					break;
				}
				$thumb = DOMCompat::querySelector( $child, '.thumb' );
				if ( !$thumb ) {
					break;
				}
				$ms = MediaStructure::parse( DOMUtils::firstNonSepChild( $thumb ) );
				if ( $ms ) {
					// FIXME: Dry all this out with T252246 / T262833
					if ( $ms->hasResource() ) {
						$resource = $ms->getResource();
						$content .= PHPUtils::stripPrefix( $resource, './' );
						// FIXME: Serializing of these attributes should
						// match the link handler so that values stashed in
						// data-mw aren't ignored.
						if ( $ms->hasAlt() ) {
							$alt = $ms->getAlt();
							$content .= '|alt=' .
								$extApi->escapeWikitext( $alt, $child, $extApi::IN_MEDIA );
						}
						// FIXME: Handle missing media
						if ( $ms->hasMediaUrl() && !$ms->isRedLink() ) {
							$href = $ms->getMediaUrl();
							if ( $href !== $resource ) {
								$href = PHPUtils::stripPrefix( $href, './' );
								$content .= '|link=' .
									$extApi->escapeWikitext( $href, $child, $extApi::IN_MEDIA );
							}
						}
					}
				} else {
					// TODO: Previously (<=1.5.0), we rendered valid titles
					// returning mw:Error (apierror-filedoesnotexist) as
					// plaintext.  Continue to serialize this content until
					// that version is no longer supported.
					$content .= $thumb->textContent;
				}
				$gallerytext = DOMCompat::querySelector( $child, '.gallerytext' );
				if ( $gallerytext ) {
					$showfilename = DOMCompat::querySelector( $gallerytext, '.galleryfilename' );
					if ( $showfilename ) {
						DOMCompat::remove( $showfilename ); // Destructive to the DOM!
					}
					$caption = $extApi->domChildrenToWikitext(
						$gallerytext, $extApi::IN_IMG_CAPTION
					);
					// Drop empty captions
					if ( !preg_match( '/^\s*$/D', $caption ) ) {
						// Ensure that this only takes one line since gallery
						// tag content is split by line
						$caption = str_replace( "\n", ' ', $caption );
						$content .= '|' . $caption;
					}
				}
				$content .= "\n";
				break;
			case XML_TEXT_NODE:
			case XML_COMMENT_NODE:
				// Ignore it
				break;
			default:
				PHPUtils::unreachable( 'should not be here!' );
				break;
			}
		}
		return $content;
	}

	/** @inheritDoc */
	public function domToWikitext(
		ParsoidExtensionAPI $extApi, Element $node, bool $wrapperUnmodified
	) {
		$dataMw = DOMDataUtils::getDataMw( $node );
		$dataMw->attrs = $dataMw->attrs ?? new stdClass;
		// Handle the "gallerycaption" first
		$galcaption = DOMCompat::querySelector( $node, 'li.gallerycaption' );
		if (
			$galcaption &&
			// FIXME: VE should signal to use the HTML by removing the
			// `caption` from data-mw.
			!is_string( $dataMw->attrs->caption ?? null )
		) {
			$dataMw->attrs->caption = $extApi->domChildrenToWikitext(
				$galcaption, $extApi::IN_IMG_CAPTION | $extApi::IN_OPTION
			);
		}
		$startTagSrc = $extApi->extStartTagToWikitext( $node );

		if ( !isset( $dataMw->body ) ) {
			return $startTagSrc; // We self-closed this already.
		} else {
			// FIXME: VE should signal to use the HTML by removing the
			// `extsrc` from the data-mw.
			if ( is_string( $dataMw->body->extsrc ?? null ) ) {
				$content = $dataMw->body->extsrc;
			} else {
				$content = $this->contentHandler( $extApi, $node );
			}
			return $startTagSrc . $content . '</' . $dataMw->name . '>';
		}
	}

	/** @inheritDoc */
	public function modifyArgDict(
		ParsoidExtensionAPI $extApi, object $argDict
	): void {
		// FIXME: Only remove after VE switches to editing HTML.
		if ( $extApi->getSiteConfig()->nativeGalleryEnabled() ) {
			// Remove extsrc from native extensions
			unset( $argDict->body->extsrc );

			// Remove the caption since it's redundant with the HTML
			// and we prefer editing it there.
			unset( $argDict->attrs->caption );
		}
	}

	/** @inheritDoc */
	public function diffHandler(
		ParsoidExtensionAPI $extApi, callable $domDiff, Element $origNode,
		Element $editedNode
	): bool {
		return call_user_func( $domDiff, $origNode, $editedNode );
	}
}
