<?php

namespace SMW\MediaWiki\Specials\SearchByProperty;

use MediaWiki\Html\Html;
use SMW\DataTypeRegistry;
use SMW\DataValueFactory;
use SMW\DataValues\StringValue;
use SMW\DIProperty;
use SMW\DIWikiPage;
use SMW\MediaWiki\MessageBuilder;
use SMW\MediaWiki\Renderer\HtmlFormRenderer;
use SMW\ProcessingErrorMsgHandler;
use SMW\Services\ServicesFactory as ApplicationFactory;
use SMWDataValue as DataValue;
use SMWInfolink as Infolink;

/**
 * @license GPL-2.0-or-later
 * @since   2.1
 *
 * @author Denny Vrandecic
 * @author Daniel Herzig
 * @author Markus Kroetzsch
 * @author mwjames
 */
class PageBuilder {

	/**
	 * @var HtmlFormRenderer
	 */
	private $htmlFormRenderer;

	/**
	 * @var PageRequestOptions
	 */
	private $pageRequestOptions;

	/**
	 * @var QueryResultLookup
	 */
	private $queryResultLookup;

	/**
	 * @var MessageBuilder
	 */
	private $messageBuilder;

	/**
	 * @var Linker
	 */
	private $linker;

	/**
	 * @since 2.1
	 *
	 * @param HtmlFormRenderer $htmlFormRenderer
	 * @param PageRequestOptions $pageRequestOptions
	 * @param QueryResultLookup $queryResultLookup
	 */
	public function __construct( HtmlFormRenderer $htmlFormRenderer, PageRequestOptions $pageRequestOptions, QueryResultLookup $queryResultLookup ) {
		$this->htmlFormRenderer = $htmlFormRenderer;
		$this->pageRequestOptions = $pageRequestOptions;
		$this->queryResultLookup = $queryResultLookup;
		$this->linker = smwfGetLinker();
	}

	/**
	 * @since 2.1
	 *
	 * @return string
	 */
	public function getHtml() {
		$this->pageRequestOptions->initialize();
		$this->messageBuilder = $this->htmlFormRenderer->getMessageBuilder();

		[ $resultMessage, $resultList, $resultCount ] = $this->getResultHtml();

		if ( ( $resultList === '' || $resultList === null ) &&
			$this->pageRequestOptions->property->getDataItem() instanceof DIProperty &&
			$this->pageRequestOptions->valueString === '' ) {
			[ $resultMessage, $resultList, $resultCount ] = $this->tryToFindAtLeastOnePropertyTableReferenceFor(
				$this->pageRequestOptions->property->getDataItem()
			);
		}

		if ( $resultList === '' || $resultList === null ) {
			$resultList = $this->messageBuilder->getMessage( 'smw_result_noresults' )->text();
		}

		$pageDescription = Html::rawElement(
			'p',
			[ 'class' => 'smw-sp-searchbyproperty-description' ],
			$this->messageBuilder->getMessage( 'smw-sp-searchbyproperty-description' )->parse()
		);

		$resultListHeader = Html::element(
			'h2',
			[],
			$this->messageBuilder->getMessage( 'smw-sp-searchbyproperty-resultlist-header' )->text()
		);

		return $pageDescription . $this->getHtmlForm( $resultMessage, $resultCount ) . $resultListHeader . $resultList;
	}

	private function getHtmlForm( $resultMessage, $resultCount ) {
		// Precaution to avoid any inline breakage caused by a div element
		// within a paragraph (e.g Highlighter content)
		$resultMessage = str_replace( 'div', 'span', $resultMessage ?? '' );

		$html = $this->htmlFormRenderer
			->setName( 'searchbyproperty' )
			->withFieldset()
			->addParagraph( $resultMessage )
			->addPaging(
				$this->pageRequestOptions->limit,
				$this->pageRequestOptions->offset,
				$resultCount )
			->addHorizontalRule()
			->addInputField(
				$this->messageBuilder->getMessage( 'smw_sbv_property' )->text(),
				'property',
				$this->pageRequestOptions->propertyString,
				'smw-property-input' )
			->addNonBreakingSpace()
			->addInputField(
				$this->messageBuilder->getMessage( 'smw_sbv_value' )->text(),
				'value',
				$this->pageRequestOptions->valueString,
				'smw-value-input' )
			->addNonBreakingSpace()
			->addSubmitButton( $this->messageBuilder->getMessage( 'smw_sbv_submit' )->text() )
			->getForm();

		return $html;
	}

	private function getResultHtml() {
		$resultList = '';
		$resultMessage = '';

		if ( $this->pageRequestOptions->propertyString === '' || !$this->pageRequestOptions->propertyString ) {
			return [ $this->messageBuilder->getMessage( 'smw_sbv_docu' )->text(), '', 0 ];
		}

		// #1728
		if ( !$this->pageRequestOptions->property->isValid() ) {
			return [ ProcessingErrorMsgHandler::getMessagesAsString( $this->pageRequestOptions->property->getErrors() ), '', 0 ];
		}

		if ( $this->pageRequestOptions->valueString !== '' && !$this->pageRequestOptions->value->isValid() ) {
			return [ ProcessingErrorMsgHandler::getMessagesAsString( $this->pageRequestOptions->value->getErrors() ), '', 0 ];
		}

		// Find out where the subject is used in connection with a query
		if ( $this->isAskQueryLinksRelatedRequest() ) {
			$exactResults = $this->queryResultLookup->doQueryLinksReferences( $this->pageRequestOptions );
			$exactCount = count( $exactResults );
			$resultList = $this->makeResultList( $exactResults, $this->pageRequestOptions->limit, true );
			return [ str_replace( '_', ' ', $resultMessage ), $resultList, $exactCount ];
		}

		$exactResults = $this->queryResultLookup->doQuery( $this->pageRequestOptions );
		$exactCount = count( $exactResults );

		if ( $this->canQueryNearbyResults( $exactCount ) ) {
			return $this->getNearbyResults( $exactResults, $exactCount );
		}

		if ( $this->pageRequestOptions->valueString === '' ) {
			$resultMessageKey = 'smw-sp-searchbyproperty-nonvaluequery';
		} else {
			$resultMessageKey = 'smw-sp-searchbyproperty-valuequery';
		}

		$resultMessage = $this->messageBuilder->getMessage(
			$resultMessageKey,
			$this->pageRequestOptions->property->getShortHTMLText( $this->linker ),
			$this->pageRequestOptions->value->getShortHTMLText( $this->linker ) )->text();

		if ( $exactCount > 0 ) {
			$resultList = $this->makeResultList( $exactResults, $this->pageRequestOptions->limit, true );
		}

		return [ str_replace( '_', ' ', $resultMessage ?? '' ), $resultList, $exactCount ];
	}

	private function getNearbyResults( $exactResults, $exactCount ) {
		$resultList = '';

		$greaterResults = $this->queryResultLookup->doQueryForNearbyResults(
			$this->pageRequestOptions,
			$exactCount,
			true
		);

		$smallerResults = $this->queryResultLookup->doQueryForNearbyResults(
			$this->pageRequestOptions,
			$exactCount,
			false
		);

		// Calculate how many greater and smaller results should be displayed
		$greaterCount = count( $greaterResults );
		$smallerCount = count( $smallerResults );

		if ( ( $greaterCount + $smallerCount + $exactCount ) > $this->pageRequestOptions->limit ) {
			$lhalf = round( ( $this->pageRequestOptions->limit - $exactCount ) / 2 );

			if ( $lhalf < $greaterCount ) {
				if ( $lhalf < $smallerCount ) {
					$smallerCount = $lhalf;
					$greaterCount = $lhalf;
				} else {
					$greaterCount = $this->pageRequestOptions->limit - ( $exactCount + $smallerCount );
				}
			} else {
				$smallerCount = $this->pageRequestOptions->limit - ( $exactCount + $greaterCount );
			}
		}

		if ( ( $greaterCount + $smallerCount + $exactCount ) == 0 ) {
			return [ '', $resultList, 0 ];
		}

		$resultMessage = $this->messageBuilder->getMessage(
			'smw_sbv_displayresultfuzzy',
			$this->pageRequestOptions->property->getShortHTMLText( $this->linker ),
			htmlspecialchars( $this->pageRequestOptions->value->getShortHTMLText( $this->linker ) )
		)->text();

		$resultList .= $this->makeResultList( $smallerResults, $smallerCount, false );

		if ( $exactCount == 0 ) {
			$resultList .= "&#160;<em><strong><small>" . $this->messageBuilder->getMessage( 'parentheses' )
				->rawParams( $this->pageRequestOptions->value->getLongHTMLText() )
				->escaped() . "</small></strong></em>";
		} else {
			$resultList .= $this->makeResultList( $exactResults, $exactCount, true, true );
		}

		$resultList .= $this->makeResultList( $greaterResults, $greaterCount, true );

		return [ $resultMessage, $resultList, $greaterCount + $exactCount ];
	}

	/**
	 * Creates the HTML for a bullet list with all the results of the set
	 * query. Values can be highlighted to show exact matches among nearby
	 * ones.
	 *
	 * @param array $results (array of (array of one or two SMWDataValues))
	 * @param int $number How many results should be displayed? -1 for all
	 * @param bool $first If less results should be displayed than
	 * 	given, should they show the first $number results, or the last
	 * 	$number results?
	 * @param bool $highlight Should the results be highlighted?
	 *
	 * @return string HTML with the bullet list, including header
	 */
	private function makeResultList( $results, $number, $first, $highlight = false ) {
		if ( $number > 0 ) {
			$results = $first ?
				array_slice( $results, 0, $number ) :
				array_slice( $results, $number );
		}

		$html = '';

		foreach ( $results as $result ) {

			$outputFormat = $result[0]->getOutputFormat();
			$result[0]->setOutputFormat( $outputFormat ? $outputFormat : 'LOCL' );

			$listitem = $result[0]->getLongHTMLText( $this->linker );

			if ( $this->canShowSearchByPropertyLink( $result[0] ) ) {

				// Copy the instance for the InfoLinker
				$res = clone $result[0];
				$res->setOutputFormat( '' );

				$value = $res instanceof StringValue && $res->getLength() < 72 ? $res->getWikiValue() : mb_substr( $res->getWikiValue(), 0, 72 );

				$listitem .= '&#160;&#160;' . Infolink::newPropertySearchLink(
					'+',
					$this->pageRequestOptions->propertyString,
					$value
				)->getHTML( $this->linker );
			} elseif ( $result[0]->getTypeID() === '_wpg' ) {

				// Add browsing link for wikipage results
				// Note: non-wikipage results are possible using inverse properties
				$listitem .= '&#160;&#160;' . Infolink::newBrowsingLink(
					'+',
					$result[0]->getLongWikiText()
				)->getHTML( $this->linker );
			}

			// Show value if not equal to the value that was searched
			// or if the current results are to be highlighted:
			if ( array_key_exists( 1, $result ) &&
				( $result[1] instanceof DataValue ) &&
				( !$result[1]->getDataItem() instanceof \SMWDIError ) &&
				( !$this->pageRequestOptions->value->getDataItem()->equals( $result[1]->getDataItem() )
					|| $highlight ) ) {

				$outputFormat = $result[1]->getOutputFormat();
				$result[1]->setOutputFormat( $outputFormat ? $outputFormat : 'LOCL' );

				$listitem .= "&#160;<em><small>" . $this->messageBuilder->getMessage( 'parentheses' )
					->rawParams( $result[1]->getLongHTMLText( $this->linker ) )
					->escaped() . "</small></em>";
			}

			// Highlight values
			if ( $highlight ) {
				$listitem = "<strong>$listitem</strong>";
			}

			$html .= "<li>$listitem</li>";
		}

		return "<ul>$html</ul>";
	}

	private function canQueryNearbyResults( $exactCount ) {
		return $exactCount < ( $this->pageRequestOptions->limit / 3 ) && $this->pageRequestOptions->nearbySearch && $this->pageRequestOptions->valueString !== '';
	}

	private function canShowSearchByPropertyLink( DataValue $dataValue ) {
		$dataTypeClass = DataTypeRegistry::getInstance()->getDataTypeClassById(
			$dataValue->getTypeID()
		);

		return $this->pageRequestOptions->value instanceof $dataTypeClass && $this->pageRequestOptions->valueString === '';
	}

	private function tryToFindAtLeastOnePropertyTableReferenceFor( DIProperty $property ) {
		$resultList = '';
		$resultMessage = '';
		$resultCount = 0;
		$extra = '';

		$dataItem = ApplicationFactory::getInstance()->getStore()->getPropertyTableIdReferenceFinder()->tryToFindAtLeastOneReferenceForProperty(
			$property
		);

		if ( !$dataItem instanceof DIWikiPage ) {
			$resultMessage = 'No reference found.';
			return [ $resultMessage, $resultList, $resultCount ];
		}

		// In case the item has already been marked as deleted but is yet pending
		// for removal
		if ( $dataItem->getInterWiki() === ':smw-delete' ) {
			$resultMessage = 'Item reference "' . $dataItem->getSubobjectName() . '" has already been marked for removal.';
			$dataItem = new DIWikiPage( $dataItem->getDBKey(), $dataItem->getNamespace() );
		}

		$dataValue = DataValueFactory::getInstance()->newDataValueByItem(
			$dataItem
		);

		$outputFormat = $dataValue->getOutputFormat();
		$dataValue->setOutputFormat( $outputFormat ? $outputFormat : 'LOCL' );

		if ( $dataValue->isValid() ) {
			// $resultMessage = 'Item reference for a zero-marked property.';
			$resultList = $dataValue->getShortHtmlText( $this->linker ) . ' ' . $extra;
			$resultCount++;

			$resultList .= '&#160;&#160;' . Infolink::newBrowsingLink(
				'+',
				$dataValue->getLongWikiText()
			)->getHTML( $this->linker );
		}

		return [ $resultMessage, $resultList, $resultCount ];
	}

	private function isAskQueryLinksRelatedRequest() {
		return $this->pageRequestOptions->property !== '' &&
			$this->pageRequestOptions->property->getDataItem()->getKey() === '_ASK' &&
			$this->pageRequestOptions->value->isValid() &&
			strpos( $this->pageRequestOptions->value->getWikiValue(), '_QUERY' ) === false;
	}

}
