<?php

namespace MediaWiki\Extension\DiscussionTools\ThreadItem;

use DateTimeImmutable;
use MediaWiki\Extension\DiscussionTools\CommentModifier;
use MediaWiki\Extension\DiscussionTools\CommentUtils;
use MediaWiki\Extension\DiscussionTools\ImmutableRange;
use MediaWiki\MediaWikiServices;
use MediaWiki\Parser\Sanitizer;
use MediaWiki\Title\Title;
use Wikimedia\Parsoid\DOM\DocumentFragment;
use Wikimedia\Parsoid\DOM\Text;
use Wikimedia\Parsoid\Utils\DOMCompat;
use Wikimedia\Parsoid\Utils\DOMUtils;

class ContentCommentItem extends ContentThreadItem implements CommentItem {
	use CommentItemTrait {
		getHeading as protected traitGetHeading;
		getSubscribableHeading as protected traitGetSubscribableHeading;
		jsonSerialize as protected traitJsonSerialize;
	}

	/** @var ImmutableRange[] */
	private array $signatureRanges;
	/** @var ImmutableRange[] */
	private array $timestampRanges;
	private DateTimeImmutable $timestamp;
	private string $author;
	private ?string $displayName;

	/**
	 * @param int $level
	 * @param ImmutableRange $range
	 * @param bool|string $transcludedFrom
	 * @param ImmutableRange[] $signatureRanges Objects describing the extent of signatures (plus
	 *  timestamps) for this comment. There is always at least one signature, but there may be
	 *  multiple. The author and timestamp of the comment is determined from the first signature.
	 * @param ImmutableRange[] $timestampRanges Objects describing the extent of timestamps within
	 *  the above signatures.
	 * @param DateTimeImmutable $timestamp
	 * @param string $author Comment author's username
	 * @param ?string $displayName Comment author's display name
	 */
	public function __construct(
		int $level, ImmutableRange $range, $transcludedFrom,
		array $signatureRanges, array $timestampRanges,
		DateTimeImmutable $timestamp,
		string $author, ?string $displayName = null
	) {
		parent::__construct( 'comment', $level, $range, $transcludedFrom );
		$this->signatureRanges = $signatureRanges;
		$this->timestampRanges = $timestampRanges;
		$this->timestamp = $timestamp;
		$this->author = $author;
		$this->displayName = $displayName;
	}

	/**
	 * @inheritDoc CommentItemTrait::jsonSerialize
	 */
	public function jsonSerialize( bool $deep = false, ?callable $callback = null ): array {
		$data = $this->traitJsonSerialize( $deep, $callback );
		if ( $this->displayName ) {
			$data['displayName'] = $this->displayName;
		}
		return $data;
	}

	/**
	 * Get the DOM of this comment's body
	 *
	 * @param bool $stripTrailingSeparator Strip a trailing separator between the body and
	 *  the signature which consists of whitespace and hyphens e.g. ' --'
	 * @return DocumentFragment Cloned fragment of the body content
	 */
	private function getBodyFragment( bool $stripTrailingSeparator = false ): DocumentFragment {
		$fragment = $this->getBodyRange()->cloneContents();
		CommentModifier::unwrapFragment( $fragment );

		if ( $stripTrailingSeparator ) {
			// Find a trailing text node
			$lastChild = $fragment->lastChild;
			while (
				$lastChild &&
				!( $lastChild instanceof Text )
			) {
				$lastChild = $lastChild->lastChild;
			}
			if (
				$lastChild instanceof Text &&
				preg_match( '/[\s\-~\x{2010}-\x{2015}\x{2043}\x{2060}]+$/u', $lastChild->nodeValue ?? '', $matches )
			) {
				$lastChild->nodeValue =
					substr( $lastChild->nodeValue ?? '', 0, -strlen( $matches[0] ) );
			}
		}
		return $fragment;
	}

	/**
	 * Get the HTML of this comment's body
	 *
	 *
	 * @param bool $stripTrailingSeparator See getBodyFragment
	 * @return string HTML
	 */
	public function getBodyHTML( bool $stripTrailingSeparator = false ): string {
		$fragment = $this->getBodyFragment( $stripTrailingSeparator );
		return DOMUtils::getFragmentInnerHTML( $fragment );
	}

	/**
	 * Get the text of this comment's body
	 *
	 * @param bool $stripTrailingSeparator See getBodyFragment
	 * @return string Text
	 */
	public function getBodyText( bool $stripTrailingSeparator = false ): string {
		$html = $this->getBodyHTML( $stripTrailingSeparator );
		return Sanitizer::stripAllTags( $html );
	}

	/**
	 * Get a list of all users mentioned
	 *
	 * @return Title[] Title objects for mentioned user pages
	 */
	public function getMentions(): array {
		$fragment = $this->getBodyRange()->cloneContents();
		// Note: DOMCompat::getElementsByTagName() doesn't take a DocumentFragment argument
		$links = DOMCompat::querySelectorAll( $fragment, 'a' );
		$users = [];
		foreach ( $links as $link ) {
			$href = $link->getAttribute( 'href' );
			if ( $href ) {
				$siteConfig = MediaWikiServices::getInstance()->getMainConfig();
				$title = Title::newFromText( CommentUtils::getTitleFromUrl( $href, $siteConfig ) );
				if ( $title && $title->inNamespace( NS_USER ) ) {
					// TODO: Consider returning User objects
					$users[] = $title;
				}
			}
		}
		return array_unique( $users );
	}

	/**
	 * @return ImmutableRange[] Comment signature ranges
	 */
	public function getSignatureRanges(): array {
		return $this->signatureRanges;
	}

	/**
	 * @return ImmutableRange[] Comment timestamp ranges
	 */
	public function getTimestampRanges(): array {
		return $this->timestampRanges;
	}

	/**
	 * @return ImmutableRange Range of the comment's "body"
	 */
	public function getBodyRange(): ImmutableRange {
		// Exclude last signature from body
		$signatureRanges = $this->getSignatureRanges();
		$lastSignature = end( $signatureRanges );
		return $this->getRange()->setEnd( $lastSignature->startContainer, $lastSignature->startOffset );
	}

	/**
	 * @return DateTimeImmutable Comment timestamp
	 */
	public function getTimestamp(): DateTimeImmutable {
		return $this->timestamp;
	}

	/**
	 * @return string Comment author
	 */
	public function getAuthor(): string {
		return $this->author;
	}

	/**
	 * @return ?string Comment display name
	 */
	public function getDisplayName(): ?string {
		return $this->displayName;
	}

	/**
	 * @inheritDoc CommentItemTrait::getHeading
	 * @suppress PhanTypeMismatchReturnSuperType
	 */
	public function getHeading(): ContentHeadingItem {
		return $this->traitGetHeading();
	}

	/**
	 * @inheritDoc CommentItemTrait::getSubscribableHeading
	 */
	public function getSubscribableHeading(): ?ContentHeadingItem {
		return $this->traitGetSubscribableHeading();
	}

	/**
	 * @param ImmutableRange $signatureRange Comment signature range to add
	 */
	public function addSignatureRange( ImmutableRange $signatureRange ): void {
		$this->signatureRanges[] = $signatureRange;
	}

	/**
	 * @param ImmutableRange[] $signatureRanges Comment signature ranges
	 */
	public function setSignatureRanges( array $signatureRanges ): void {
		$this->signatureRanges = $signatureRanges;
	}

	/**
	 * @param DateTimeImmutable $timestamp Comment timestamp
	 */
	public function setTimestamp( DateTimeImmutable $timestamp ): void {
		$this->timestamp = $timestamp;
	}

	/**
	 * @param string $author Comment author
	 */
	public function setAuthor( string $author ): void {
		$this->author = $author;
	}
}