<?php
declare( strict_types = 1 );

namespace MediaWiki\Extension\Translate\TtmServer;

use CrossLanguageTranslationSearchQuery;
use ErrorPageError;
use FormatJson;
use FormOptions;
use Html;
use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroups;
use MediaWiki\Extension\Translate\TranslatorInterface\Aid\CurrentTranslationAid;
use MediaWiki\Extension\Translate\TranslatorInterface\Aid\TranslationAidDataProvider;
use MediaWiki\Extension\Translate\Utilities\Utilities;
use MediaWiki\Languages\LanguageFactory;
use MediaWiki\Languages\LanguageNameUtils;
use Message;
use MessageHandle;
use SearchableTTMServer;
use SpecialPage;
use Title;
use TTMServerException;
use WikiMap;
use Xml;

/**
 * Contains logic to search for translations
 *
 * @author Niklas Laxström
 * @license GPL-2.0-or-later
 * @ingroup SpecialPage TranslateSpecialPage
 */
class SearchTranslationsSpecialPage extends SpecialPage {
	/** @var FormOptions */
	protected $opts;
	/**
	 * Placeholders used for highlighting. Search backend can mark the beginning and
	 * end but we need to run htmlspecialchars on the result first and then
	 * replace the placeholders with the html. It is assumed placeholders
	 * don't contain any chars that are escaped in html.
	 * @var array
	 */
	protected $hl = [];
	/**
	 * How many search results to display per page
	 * @var int
	 */
	protected $limit = 25;
	/** @var TtmServerFactory */
	private $ttmServerFactory;
	/** @var LanguageFactory */
	private $languageFactory;

	public function __construct(
		TtmServerFactory $ttmServerFactory,
		LanguageFactory $languageFactory
	) {
		parent::__construct( 'SearchTranslations' );
		$this->hl = [
			Utilities::getPlaceholder(),
			Utilities::getPlaceholder(),
		];

		$this->ttmServerFactory = $ttmServerFactory;
		$this->languageFactory = $languageFactory;
	}

	public function execute( $par ) {
		global $wgLanguageCode;
		$this->setHeaders();
		$this->checkPermissions();

		$server = $this->ttmServerFactory->getDefault();
		if ( !$server instanceof SearchableTTMServer ) {
			throw new ErrorPageError( 'tux-sst-nosolr-title', 'tux-sst-nosolr-body' );
		}

		$out = $this->getOutput();
		$out->addModuleStyles( 'jquery.uls.grid' );
		$out->addModuleStyles( 'ext.translate.specialpages.styles' );
		$out->addModuleStyles( 'ext.translate.special.translate.styles' );
		$out->addModuleStyles( [ 'mediawiki.ui.button', 'mediawiki.ui.input', 'mediawiki.ui.checkbox' ] );
		$out->addModules( 'ext.translate.special.searchtranslations' );
		$out->addModules( 'ext.translate.special.searchtranslations.operatorsuggest' );
		$out->addHelpLink( 'Help:Extension:Translate#searching' );
		$out->addJsConfigVars(
			'wgTranslateLanguages',
			Utilities::getLanguageNames( LanguageNameUtils::AUTONYMS )
		);

		$this->opts = $opts = new FormOptions();
		$opts->add( 'query', '' );
		$opts->add( 'sourcelanguage', $wgLanguageCode );
		$opts->add( 'language', '' );
		$opts->add( 'group', '' );
		$opts->add( 'grouppath', '' );
		$opts->add( 'filter', '' );
		$opts->add( 'match', '' );
		$opts->add( 'case', '' );
		$opts->add( 'limit', $this->limit );
		$opts->add( 'offset', 0 );

		$opts->fetchValuesFromRequest( $this->getRequest() );

		$queryString = $opts->getValue( 'query' );

		if ( $queryString === '' ) {
			$this->showEmptySearch();
			return;
		}

		$search = $this->getSearchInput( $queryString );

		$options = $params = $opts->getAllValues();
		$filter = $opts->getValue( 'filter' );
		try {
			if ( $opts->getValue( 'language' ) === '' ) {
				$options['language'] = $this->getLanguage()->getCode();
			}
			$translationSearch = new CrossLanguageTranslationSearchQuery( $options, $server );
			if ( in_array( $filter, $translationSearch->getAvailableFilters() ) ) {
				if ( $options['language'] === $options['sourcelanguage'] ) {
					$this->showSearchError( $search, $this->msg( 'tux-sst-error-language' ) );
					return;
				}

				$opts->setValue( 'language', $options['language'] );
				$documents = $translationSearch->getDocuments();
				$total = $translationSearch->getTotalHits();
				$resultset = $translationSearch->getResultSet();
			} else {
				$resultset = $server->search( $queryString, $params, $this->hl );
				$documents = $server->getDocuments( $resultset );
				$total = $server->getTotalHits( $resultset );
			}
		} catch ( TTMServerException $e ) {
			$message = $e->getMessage();
			// Known exceptions
			if ( preg_match( '/^Result window is too large/', $message ) ) {
				$this->showSearchError( $search, $this->msg( 'tux-sst-error-offset' ) );
				return;
			}

			// Other exceptions
			error_log( 'Translation search server unavailable: ' . $e->getMessage() );
			throw new ErrorPageError( 'tux-sst-solr-offline-title', 'tux-sst-solr-offline-body' );
		}

		// Part 1: facets
		$facets = $server->getFacets( $resultset );
		$facetHtml = '';

		if ( $facets['language'] !== [] ) {
			if ( $filter !== '' ) {
				$facets['language'] = array_merge(
					$facets['language'],
					[ $opts->getValue( 'language' ) => $total ]
				);
			}
			$facetHtml = Html::element( 'div',
				[ 'class' => 'row facet languages',
					'data-facets' => FormatJson::encode( $this->getLanguages( $facets['language'] ) ),
					'data-language' => $opts->getValue( 'language' ),
				],
				$this->msg( 'tux-sst-facet-language' )->text()
			);
		}

		if ( $facets['group'] !== [] ) {
			$facetHtml .= Html::element( 'div',
				[ 'class' => 'row facet groups',
					'data-facets' => FormatJson::encode( $this->getGroups( $facets['group'] ) ),
					'data-group' => $opts->getValue( 'group' ) ],
				$this->msg( 'tux-sst-facet-group' )->text()
			);
		}

		// Part 2: results
		$resultsHtml = '';

		$title = Title::newFromText( $queryString );
		if ( $title && !in_array( $filter, $translationSearch->getAvailableFilters() ) ) {
			$handle = new MessageHandle( $title );
			$code = $handle->getCode();
			$language = $opts->getValue( 'language' );
			if ( $code !== '' && $code !== $language && $handle->isValid() ) {
				$dataProvider = new TranslationAidDataProvider( $handle );
				$aid = new CurrentTranslationAid(
					$handle->getGroup(),
					$handle,
					$this->getContext(),
					$dataProvider
				);
				$document = [
					'wiki' => WikiMap::getCurrentWikiId(),
					'localid' => $handle->getTitleForBase()->getPrefixedText(),
					'content' => $aid->getData()['value'],
					'language' => $handle->getCode(),
				];
				array_unshift( $documents, $document );
				$total++;
			}
		}

		foreach ( $documents as $document ) {
			$text = $document['content'];
			$text = Utilities::convertWhiteSpaceToHTML( $text );

			list( $pre, $post ) = $this->hl;
			$text = str_replace( $pre, '<strong class="tux-search-highlight">', $text );
			$text = str_replace( $post, '</strong>', $text );

			$title = Title::newFromText( $document['localid'] . '/' . $document['language'] );
			if ( !$title ) {
				// Should not ever happen but who knows...
				continue;
			}

			$resultAttribs = [
				'class' => 'row tux-message',
				'data-title' => $title->getPrefixedText(),
				'data-language' => $document['language'],
			];

			$handle = new MessageHandle( $title );

			if ( $handle->isValid() ) {
				$uri = Utilities::getEditorUrl( $handle );
				$link = Html::element(
					'a',
					[ 'href' => $uri ],
					$this->msg( 'tux-sst-edit' )->text()
				);
			} else {
				$url = wfParseUrl( $document['uri'] );
				$domain = $url['host'];
				$link = Html::element(
					'a',
					[ 'href' => $document['uri'] ],
					$this->msg( 'tux-sst-view-foreign', $domain )->text()
				);
			}

			$access = Html::rawElement(
				'div',
				[ 'class' => 'row tux-edit tux-message-item' ],
				$link
			);

			$titleText = $title->getPrefixedText();
			$titleAttribs = [
				'class' => 'row tux-title',
				'dir' => 'ltr',
			];

			$language = $this->languageFactory->getLanguage( $document['language'] );
			$textAttribs = [
				'class' => 'row tux-text',
				'lang' => $language->getHtmlCode(),
				'dir' => $language->getDir(),
			];

			$resultsHtml .= Html::openElement( 'div', $resultAttribs )
				. Html::rawElement( 'div', $textAttribs, $text )
				. Html::element( 'div', $titleAttribs, $titleText )
				. $access
				. Html::closeElement( 'div' );
		}

		$resultsHtml .= Html::rawElement( 'hr', [ 'class' => 'tux-pagination-line' ] );

		$prev = $next = '';
		$offset = $this->opts->getValue( 'offset' );
		$params = $this->opts->getChangedValues();

		if ( $total - $offset > $this->limit ) {
			$newParams = [ 'offset' => $offset + $this->limit ] + $params;
			$attribs = [
				'class' => 'mw-ui-button pager-next',
				'href' => $this->getPageTitle()->getLocalURL( $newParams ),
			];
			$next = Html::element( 'a', $attribs, $this->msg( 'tux-sst-next' )->text() );
		}
		if ( $offset ) {
			$newParams = [ 'offset' => max( 0, $offset - $this->limit ) ] + $params;
			$attribs = [
				'class' => 'mw-ui-button pager-prev',
				'href' => $this->getPageTitle()->getLocalURL( $newParams ),
			];
			$prev = Html::element( 'a', $attribs, $this->msg( 'tux-sst-prev' )->text() );
		}

		$resultsHtml .= Html::rawElement( 'div', [ 'class' => 'tux-pagination-links' ],
			"$prev $next"
		);

		$count = $this->msg( 'tux-sst-count' )->numParams( $total )->escaped();

		$this->showSearch( $search, $count, $facetHtml, $resultsHtml, $total );
	}

	private function getLanguages( array $facet ): array {
		$output = [];

		$nondefaults = $this->opts->getChangedValues();
		$selected = $this->opts->getValue( 'language' );
		$filter = $this->opts->getValue( 'filter' );

		foreach ( $facet as $key => $value ) {
			if ( $filter !== '' && $key === $selected ) {
				unset( $nondefaults['language'] );
				unset( $nondefaults['filter'] );
			} elseif ( $filter !== '' ) {
				$nondefaults['language'] = $key;
				$nondefaults['filter'] = $filter;
			} elseif ( $key === $selected ) {
				unset( $nondefaults['language'] );
			} else {
				$nondefaults['language'] = $key;
			}

			$url = $this->getPageTitle()->getLocalURL( $nondefaults );
			$value = $this->getLanguage()->formatNum( $value );

			$output[$key] = [
				'count' => $value,
				'url' => $url
			];
		}

		return $output;
	}

	private function getGroups( array $facet ): array {
		$structure = MessageGroups::getGroupStructure();
		return $this->makeGroupFacetRows( $structure, $facet );
	}

	private function makeGroupFacetRows(
		array $groups,
		array $counts,
		int $level = 0,
		string $pathString = ''
	): array {
		$output = [];

		$nondefaults = $this->opts->getChangedValues();
		$selected = $this->opts->getValue( 'group' );
		$path = explode( '|', $this->opts->getValue( 'grouppath' ) );

		foreach ( $groups as $mixed ) {
			$subgroups = $group = $mixed;

			if ( is_array( $mixed ) ) {
				$group = array_shift( $subgroups );
			} else {
				$subgroups = [];
			}
			'@phan-var \MessageGroup $group';
			$id = $group->getId();

			if ( $id !== $selected && !isset( $counts[$id] ) ) {
				continue;
			}

			if ( $id === $selected ) {
				unset( $nondefaults['group'] );
				$nondefaults['grouppath'] = $pathString;
			} else {
				$nondefaults['group'] = $id;
				$nondefaults['grouppath'] = $pathString . $id;
			}

			$value = $counts[$id] ?? 0;

			$output[$id] = [
				'id' => $id,
				'count' => $value,
				'label' => $group->getLabel(),
			];

			if ( isset( $path[$level] ) && $path[$level] === $id ) {
				$output[$id]['groups'] = $this->makeGroupFacetRows(
					$subgroups,
					$counts,
					$level + 1,
					"$pathString$id|"
				);
			}
		}

		return $output;
	}

	private function showSearch(
		string $search,
		string $count,
		string $facets,
		string $results,
		int $total
	): void {
		$messageSelector = $this->messageSelector();
		$this->getOutput()->addHTML(
			<<<HTML
			<div class="grid tux-searchpage">
				<div class="row tux-searchboxform">
					<div class="tux-search-tabs offset-by-three">$messageSelector</div>
					<div class="row tux-search-options">
						<div class="offset-by-three nine columns tux-search-inputs">
							<div class="row searchinput">$search</div>
							<div class="row count">$count</div>
						</div>
					</div>
				</div>
			HTML
		);

		$query = trim( $this->opts->getValue( 'query' ) );
		$hasSpace = preg_match( '/\s/', $query );
		$match = $this->opts->getValue( 'match' );
		$size = 100;
		if ( $total > $size && $match !== 'all' && $hasSpace ) {
			$params = $this->opts->getChangedValues();
			$params = [ 'match' => 'all' ] + $params;
			$linkText = $this->msg( 'tux-sst-link-all-match' )->text();
			$link = $this->getPageTitle()->getFullURL( $params );
			$link = "<span class='plainlinks'>[$link $linkText]</span>";

			$out = $this->getOutput();
			$out->addHTML(
				Html::successBox(
					$out->msg( 'tux-sst-match-message', $link )->parse()
				)
			);
		}

		$this->getOutput()->addHTML(
			<<<HTML
				<div class="row searchcontent">
					<div class="three columns facets">$facets</div>
					<div class="nine columns results">$results</div>
				</div>
			</div>
			HTML
		);
	}

	private function showEmptySearch(): void {
		$search = $this->getSearchInput( '' );
		$this->getOutput()->addHTML(
			<<<HTML
			<div class="grid tux-searchpage">
				<div class="row searchinput">
					<div class="nine columns offset-by-three">$search</div>
				</div>
			</div>
			HTML
		);
	}

	private function showSearchError( string $search, Message $message ): void {
		$messageSelector = $this->messageSelector();
		$messageHTML = Html::errorBox(
			$message->escaped(),
			'',
			'row'
		);
		$this->getOutput()->addHTML(
			<<<HTML
			<div class="grid tux-searchpage">
				<div class="row tux-searchboxform">
					<div class="tux-search-tabs offset-by-three">$messageSelector</div>
					<div class="row tux-search-options">
						<div class="offset-by-three nine columns tux-search-inputs">
							<div class="row searchinput">$search</div>
							$messageHTML
						</div>
					</div>
				</div>
			</div>
			HTML
		);
	}

	/** Build ellipsis to select options */
	private function ellipsisSelector( string $key, string $value ): string {
		$nondefaults = $this->opts->getChangedValues();
		$taskParams = [ 'filter' => $value ] + $nondefaults;
		ksort( $taskParams );
		$href = $this->getPageTitle()->getLocalURL( $taskParams );
		$link = Html::element( 'a',
			[ 'href' => $href ],
			// Messages for grepping:
			// tux-sst-ellipsis-untranslated
			// tux-sst-ellipsis-outdated
			$this->msg( 'tux-sst-ellipsis-' . $key )->text()
		);

		$container = Html::rawElement( 'li', [
			'class' => 'column',
			'data-filter' => $value,
			'data-title' => $key,
		], $link );

		return $container;
	}

	/** Design the tabs */
	private function messageSelector(): string {
		$nondefaults = $this->opts->getChangedValues();
		$output = Html::openElement( 'div', [ 'class' => 'row tux-messagetable-header' ] );
		$output .= Html::openElement( 'div', [ 'class' => 'nine columns' ] );
		$output .= Html::openElement( 'ul', [ 'class' => 'row tux-message-selector' ] );
		$tabs = [
			'default' => '',
			'translated' => 'translated',
			'untranslated' => 'untranslated'
		];

		$ellipsisOptions = [
			'outdated' => 'fuzzy'
		];

		$selected = $this->opts->getValue( 'filter' );
		if ( in_array( $selected, $ellipsisOptions ) ) {
			$ellipsisOptions = array_slice( $tabs, -1 );

			// Remove the last tab
			array_pop( $tabs );
			$tabs = array_merge( $tabs, [ 'outdated' => $selected ] );
		} elseif ( !in_array( $selected, $tabs ) ) {
			$selected = '';
		}

		$container = Html::openElement( 'ul', [ 'class' => 'column tux-message-selector' ] );
		foreach ( $ellipsisOptions as $optKey => $optValue ) {
			$container .= $this->ellipsisSelector( $optKey, $optValue );
		}

		$sourcelanguage = $this->opts->getValue( 'sourcelanguage' );
		$sourcelanguage = Utilities::getLanguageName( $sourcelanguage );
		foreach ( $tabs as $tab => $filter ) {
			// Messages for grepping:
			// tux-sst-default
			// tux-sst-translated
			// tux-sst-untranslated
			// tux-sst-outdated
			$tabClass = "tux-sst-$tab";
			$taskParams = [ 'filter' => $filter ] + $nondefaults;
			ksort( $taskParams );
			$href = $this->getPageTitle()->getLocalURL( $taskParams );
			if ( $tab === 'default' ) {
				$link = Html::element(
					'a',
					[ 'href' => $href ],
					$this->msg( $tabClass )->text()
				);
			} else {
				$link = Html::element(
					'a',
					[ 'href' => $href ],
					$this->msg( $tabClass, $sourcelanguage )->text()
				);
			}

			if ( $selected === $filter ) {
				$tabClass .= ' selected';
			}
			$output .= Html::rawElement( 'li', [
				'class' => [ 'column', $tabClass ],
				'data-filter' => $filter,
				'data-title' => $tab,
			], $link );
		}

		// More column
		$output .= Html::openElement( 'li', [ 'class' => 'column more' ] ) .
			'...' .
			$container .
			Html::closeElement( 'li' );

		$output .= Html::closeElement( 'ul' ) . Html::closeElement( 'div' ) . Html::closeElement( 'div' );

		return $output;
	}

	private function getSearchInput( string $query ) {
		$attribs = [
			'placeholder' => $this->msg( 'tux-sst-search-ph' )->text(),
			'class' => 'searchinputbox mw-ui-input',
			'dir' => $this->getLanguage()->getDir(),
		];

		$title = Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() );
		$input = Xml::input( 'query', false, $query, $attribs );
		$submit = Xml::submitButton(
			$this->msg( 'tux-sst-search' )->text(),
			[ 'class' => 'mw-ui-button mw-ui-progressive' ]
		);

		$nondefaults = $this->opts->getChangedValues();
		$checkLabel = Xml::checkLabel(
			$this->msg( 'tux-sst-case-sensitive' )->text(),
			'case',
			'tux-case-sensitive',
			isset( $nondefaults['case'] )
		);
		$checkLabel = Html::openElement(
			'div',
			[ 'class' => 'tux-search-operators mw-ui-checkbox' ]
		) .
			$checkLabel .
			Html::closeElement( 'div' );

		$lang = $this->getRequest()->getVal( 'language' );
		$language = $lang === null ? '' : Html::hidden( 'language', $lang );

		$form = Html::rawElement( 'form', [ 'action' => wfScript(), 'name' => 'searchform' ],
			$title . $input . $submit . $checkLabel . $language
		);

		return $form;
	}

	protected function getGroupName() {
		return 'translation';
	}
}
