<?php

use ParamProcessor\Options;
use ParamProcessor\ParamDefinition;
use ParamProcessor\ProcessedParam;
use ParamProcessor\Processor;
use SMW\Parser\RecursiveTextProcessor;
use SMW\Query\Deferred;
use SMW\Query\Exception\ResultFormatNotFoundException;
use SMW\Query\PrintRequest;
use SMW\Query\Processor\DefaultParamDefinition;
use SMW\Query\Processor\ParamListProcessor;
use SMW\Query\QueryContext;
use SMW\Query\QueryResult;
use SMW\Query\ResultFormat;
use SMW\Query\ResultPrinter;
use SMW\Query\ResultPrinterDependency;
use SMW\Query\ResultPrinters\NullResultPrinter;
use SMW\Services\ServicesFactory as ApplicationFactory;

/**
 * Static class for accessing functions to generate and execute semantic queries
 * and to serialise their results.
 * @ingroup SMWQuery
 */
class SMWQueryProcessor implements QueryContext {

	/**
	 * @var RecursiveTextProcessor
	 */
	private static $recursiveTextProcessor;

	/**
	 * @since 3.0
	 *
	 * @param RecursiveTextProcessor|null $recursiveTextProcessor
	 */
	public static function setRecursiveTextProcessor( ?RecursiveTextProcessor $recursiveTextProcessor = null ) {
		self::$recursiveTextProcessor = $recursiveTextProcessor;
	}

	/**
	 * Takes an array of unprocessed parameters, processes them using
	 * Validator, and returns them.
	 *
	 * Both input and output arrays are
	 * param name (string) => param value (mixed)
	 *
	 * @since 1.6.2
	 * The return value changed in SMW 3.1 from Param[] to ProcessedParam[]
	 *
	 * @param array $params
	 * @param PrintRequest[] $printRequests
	 * @param bool $unknownInvalid
	 *
	 * @return ProcessedParam[]
	 */
	public static function getProcessedParams( array $params, array $printRequests = [], $unknownInvalid = true, $context = null, $showMode = false ) {
		// #4261
		// The `ProcessedParam` library creates inconsistent results pending the
		// input types especially between conversion of string to integer, so to
		// ensure that method outputs a consistent behaviour independent of the
		// type, we recast offset/limit as string.
		//
		// We remove the string casts tests would start to fail!

		if ( isset( $params['offset'] ) ) {
			$params['offset'] = (string)$params['offset'];
		}

		if ( isset( $params['limit'] ) ) {
			$params['limit'] = (string)$params['limit'];
		}

		$validator = self::getProcessorForParams( $params, $printRequests, $unknownInvalid, $context, $showMode );
		$processingResult = $validator->processParameters();

		$parameters = [];

		foreach ( $processingResult->getParameters() as $parameter ) {
			$parameters[$parameter->getName()] = $parameter;
		}

		// Set limit parameter to its original value.
		// This is to allow for special handling of invalid values rather than just setting the parameter to its default.
		// $smwgQMaxLimit and $smwgQMaxInlineLimit will be applied at a later point.
		if ( isset( $params['limit'] ) && isset( $parameters['limit'] ) ) {
			$parameters['limit'] = new ProcessedParam(
				'limit',
				(int)$params['limit'],
				false,
				'limit',
				(int)$params['limit']
			);
		}

		return $parameters;
	}

	/**
	 * Parse a query string given in SMW's query language to create
	 * an SMWQuery. Parameters are given as key-value-pairs in the
	 * given array. The parameter $context defines in what context the
	 * query is used, which affects ceretain general settings.
	 * An object of type SMWQuery is returned.
	 *
	 * The format string is used to specify the output format if already
	 * known. Otherwise it will be determined from the parameters when
	 * needed. This parameter is just for optimisation in a common case.
	 *
	 * @param string $queryString
	 * @param array $params These need to be the result of a list fed to getProcessedParams
	 * @param $context
	 * @param string $format
	 * @param array $extraPrintouts
	 *
	 * @return SMWQuery
	 */
	public static function createQuery( $queryString, array $params, $context = self::INLINE_QUERY, $format = '', array $extraPrintouts = [], $contextPage = null ) {
		if ( $format === '' || $format === null ) {
			$format = $params['format']->getValue();
		}

		$defaultSort = 'ASC';

		if ( $format == 'count' ) {
			$queryMode = SMWQuery::MODE_COUNT;
		} elseif ( $format == 'debug' ) {
			$queryMode = SMWQuery::MODE_DEBUG;
		} else {
			$printer = self::getResultPrinter( $format, $context );
			$queryMode = $printer->getQueryMode( $context );
			$defaultSort = $printer->getDefaultSort();
		}

		// set mode, limit, and offset:
		$offset = 0;
		$limit = $GLOBALS['smwgQDefaultLimit'];

		if ( ( array_key_exists( 'offset', $params ) ) && ( is_int( $params['offset']->getValue() + 0 ) ) ) {
			$offset = $params['offset']->getValue();
		}

		if ( ( array_key_exists( 'limit', $params ) ) && ( is_int( trim( $params['limit']->getValue() ) + 0 ) ) ) {
			$limit = $params['limit']->getValue();

			// limit < 0: always show further results link only
			if ( ( trim( $params['limit']->getValue() ) + 0 ) < 0 ) {
				$queryMode = SMWQuery::MODE_NONE;
			}
		}

		// largest possible limit for "count", even inline
		if ( $queryMode == SMWQuery::MODE_COUNT ) {
			$offset = 0;
			$limit = $GLOBALS['smwgQMaxLimit'];
		}

		$queryCreator = ApplicationFactory::getInstance()->singleton( 'QueryCreator' );

		$params = [
			'extraPrintouts' => $extraPrintouts,
			'queryMode'   => $queryMode,
			'context'     => $context,
			'contextPage' => $contextPage,
			'offset'      => $offset,
			'limit'       => $limit,
			'source'      => $params['source']->getValue(),
			'mainLabel'   => $params['mainlabel']->getValue(),
			'sort'        => $params['sort']->getValue(),
			'order'       => $params['order']->getValue(),
			'defaultSort' => $defaultSort
		];

		return $queryCreator->create( $queryString, $params );
	}

	/**
	 * Add the subject print request, unless mainlabel is set to "-".
	 *
	 * @since 1.7
	 *
	 * @param array &$printRequests
	 * @param array $rawParams
	 */
	public static function addThisPrintout( array &$printRequests, array $rawParams ) {
		if ( $printRequests === null ) {
			return;
		}

		// If THIS is already registered, bail-out!
		foreach ( $printRequests as $printRequest ) {
			if ( $printRequest->isMode( PrintRequest::PRINT_THIS ) ) {
				return;
			}
		}

		$hasMainlabel = array_key_exists( 'mainlabel', $rawParams );

		if ( !$hasMainlabel || trim( $rawParams['mainlabel'] ) !== '-' ) {
			$printRequest = new PrintRequest(
				PrintRequest::PRINT_THIS,
				$hasMainlabel ? $rawParams['mainlabel'] : ''
			);

			// Signal to any post-processing that THIS was added outside of
			// the normal processing chain
			$printRequest->isDisconnected( true );

			array_unshift( $printRequests, $printRequest );
		}
	}

	/**
	 * Preprocess a query as given by an array of parameters as is
	 * typically produced by the #ask parser function or by Special:Ask.
	 * The parsing results in a querystring, an array of additional
	 * parameters, and an array of additional \SMW\Query\PrintRequest objects,
	 * which are returned in an array with three components. If
	 * $showMode is true then the input will be processed as if for #show.
	 * This uses a slightly different way to get the query, and different
	 * default labels (empty) for additional print requests.
	 *
	 * @param array $rawParams
	 * @param bool $showMode
	 * @return array( string, array( string => string ), array( \SMW\Query\PrintRequest ) )
	 */
	public static function getComponentsFromFunctionParams( array $rawParams, $showMode ) {
		/**
		 * @var ParamListProcessor $paramListProcessor
		 */
		$paramListProcessor = ApplicationFactory::getInstance()->singleton( 'ParamListProcessor' );

		return $paramListProcessor->format(
			$paramListProcessor->preprocess( $rawParams, $showMode ),
			ParamListProcessor::FORMAT_LEGACY
		);
	}

	/**
	 * Process and answer a query as given by an array of parameters as is
	 * typically produced by the #ask parser function. The parameter
	 * $context defines in what context the query is used, which affects
	 * certain general settings.
	 *
	 * The main task of this function is to preprocess the raw parameters
	 * to obtain actual parameters, printout requests, and the query string
	 * for further processing.
	 *
	 * @since 1.8
	 * @param array $rawParams user-provided list of unparsed parameters
	 * @param int $outputMode SMW_OUTPUT_WIKI, SMW_OUTPUT_HTML, ...
	 * @param int $context INLINE_QUERY, SPECIAL_PAGE, CONCEPT_DESC
	 * @param bool $showMode process like #show parser function?
	 * @return array( SMWQuery, ProcessedParam[] )
	 */
	public static function getQueryAndParamsFromFunctionParams( array $rawParams, $outputMode, $context, $showMode, $contextPage = null ) {
		[ $queryString, $params, $printouts ] = self::getComponentsFromFunctionParams( $rawParams, $showMode );

		if ( !$showMode ) {
			self::addThisPrintout( $printouts, $params );
		}

		$params = self::getProcessedParams( $params, $printouts, true, $context, $showMode );

		$query = self::createQuery( $queryString, $params, $context, '', $printouts, $contextPage );

		// For convenience keep parameters and options to be available for immediate
		// processing
		if ( $context === self::DEFERRED_QUERY ) {
			$query->setOption( Deferred::QUERY_PARAMETERS, implode( '|', $rawParams ) );
			$query->setOption( Deferred::SHOW_MODE, $showMode );
			$query->setOption( Deferred::CONTROL_ELEMENT, isset( $params['@control'] ) ? $params['@control']->getValue() : '' );
		}

		return [ $query, $params ];
	}

	/**
	 * Create a fully formatted result string from a query and its
	 * parameters. The method takes care of processing various types of
	 * query result. Most cases are handled by printers, but counting and
	 * debugging uses special code.
	 *
	 * @param SMWQuery $query
	 * @param array $params These need to be the result of a list fed to getProcessedParams
	 * @param int $outputMode
	 * @param int $context
	 * @since before 1.7, but public only since 1.8
	 *
	 * @return string
	 */
	public static function getResultFromQuery( SMWQuery $query, array $params, $outputMode, $context ) {
		$printer = self::getResultPrinter(
			$params['format']->getValue(),
			$context
		);

		if ( $printer instanceof ResultPrinterDependency && $printer->hasMissingDependency() ) {
			return $printer->getDependencyError();
		}

		if ( $printer->isDeferrable() && $context === self::DEFERRED_QUERY && $query->getLimit() > 0 ) {

			// Halt processing that is not `DEFERRED_DATA` as it is expected the
			// process is picked-up by the `deferred.js` loader that will
			// initiate an API request to finalize the query after MW has build
			// the page.
			if ( $printer->isDeferrable() !== $printer::DEFERRED_DATA ) {
				return Deferred::buildHTML( $query );
			}

			// `DEFERRED_DATA` is interpret as "execute the query with limit=0" (i.e.
			// no query execution) but allow the printer to setup the HTML so that
			// the data can be loaded after MW has finished the page build including
			// the pre-rendered query HTML representation. This mode deferrers the
			// actual query execution and data load to after the page build.
			//
			// Each printer that uses this mode has to handle the required parameters
			// and data load accordingly.
			$query->querymode = SMWQuery::MODE_INSTANCES;
			$query->setOption( 'deferred.limit', $query->getLimit() );
			$query->setLimit( 0 );
		}

		$querySourceFactory = ApplicationFactory::getInstance()->getQuerySourceFactory();
		$source = $params['source']->getValue();

		if ( $source === '' && $context === self::CURTAILMENT_MODE ) {
			$querySource = $querySourceFactory->newSingleEntityQueryLookup();
		} else {
			$querySource = $querySourceFactory->get( $source );
		}

		$res = $querySource->getQueryResult( $query );
		$start = microtime( true );

		if ( $res instanceof QueryResult && $query->getOption( 'calc.result_hash' ) ) {
			$query->setOption( 'result_hash', $res->getHash( QueryResult::QUICK_HASH ) );
		}

		if ( ( $query->querymode == SMWQuery::MODE_INSTANCES ) ||
			( $query->querymode == SMWQuery::MODE_NONE ) ) {

			$result = $printer->getResult( $res, $params, $outputMode );

			$query->setOption( SMWQuery::PROC_PRINT_TIME, microtime( true ) - $start );
			return $result;
		} else { // result for counting or debugging is just a string or number

			if ( $res instanceof QueryResult ) {
				$res = $res->getCountValue();
			}

			if ( is_numeric( $res ) ) {
				$res = strval( $res );
			}

			if ( is_string( $res ) ) {
				$result = str_replace( '_', ' ', $params['intro']->getValue() )
					. $res
					. str_replace( '_', ' ', $params['outro']->getValue() )
					. smwfEncodeMessages( $query->getErrors() );
			} else {
				// When no valid result was obtained, $res will be a QueryResult.
				$result = smwfEncodeMessages( $query->getErrors() );
			}

			$query->setOption( SMWQuery::PROC_PRINT_TIME, microtime( true ) - $start );

			return $result;
		}
	}

	/**
	 * Find suitable SMWResultPrinter for the given format. The context in
	 * which the query is to be used determines some basic settings of the
	 * returned printer object. Possible contexts are
	 * SMWQueryProcessor::SPECIAL_PAGE, SMWQueryProcessor::INLINE_QUERY,
	 * SMWQueryProcessor::CONCEPT_DESC.
	 *
	 * @param string $format
	 * @param $context
	 *
	 * @return ResultPrinter
	 * @throws ResultFormatNotFoundException
	 */
	public static function getResultPrinter( $format, $context = self::SPECIAL_PAGE ) {
		global $smwgResultFormats;

		ResultFormat::resolveFormatAliases( $format );

		if ( !array_key_exists( $format, $smwgResultFormats ) ) {
			throw new ResultFormatNotFoundException( "There is no result format for '$format'." );
		}

		$formatClass = $smwgResultFormats[$format];

		/**
		 * @var ResultPrinter $printer
		 */
		$printer = new $formatClass( $format, ( $context != self::SPECIAL_PAGE ) );

		if ( self::$recursiveTextProcessor === null ) {
			self::$recursiveTextProcessor = new RecursiveTextProcessor();
		}

		$printer->setRecursiveTextProcessor(
			self::$recursiveTextProcessor
		);

		return $printer;
	}

	/**
	 * Produces a list of default allowed parameters for a result printer. Most
	 * query printers should override this function.
	 *
	 * @since 1.6.2, return element type changed in 1.8
	 *
	 * @param int|null $context
	 * @param ResultPrinter|null $resultPrinter
	 *
	 * @return ParamDefinition[]
	 */
	public static function getParameters( $context = null, $resultPrinter = null ) {
		return DefaultParamDefinition::getParamDefinitions( $context, $resultPrinter );
	}

	/**
	 * Returns the definitions of all parameters supported by the specified format.
	 *
	 * @since 1.8
	 *
	 * @param string $format
	 *
	 * @return ParamDefinition[]
	 */
	public static function getFormatParameters( $format ) {
		ResultFormat::resolveFormatAliases( $format );

		if ( !array_key_exists( $format, $GLOBALS['smwgResultFormats'] ) ) {
			return [];
		}

		$resultPrinter = self::getResultPrinter( $format );

		if ( $resultPrinter instanceof NullResultPrinter ) {
			return [];
		}

		return ParamDefinition::getCleanDefinitions(
			$resultPrinter->getParamDefinitions( self::getParameters( null, $resultPrinter ) )
		);
	}

	/**
	 * Takes an array of unprocessed parameters,
	 * and sets them on a new Validator object,
	 * which is returned and ready to process the parameters.
	 *
	 * @param array $params
	 * @param PrintRequest[] $printRequests
	 * @param bool $unknownInvalid
	 *
	 * @return Processor
	 */
	private static function getProcessorForParams( array $params, array $printRequests = [], $unknownInvalid = true, $context = null, $showMode = false ) {
		$paramDefinitions = self::getParameters( $context );

		/**
		 * @var ResultFormat $formatParameter
		 */
		$formatParameter = $paramDefinitions['format'];

		$formatParameter->setPrintRequests( $printRequests );
		$formatParameter->setShowMode( $showMode );

		$processorOptions = new Options();
		$processorOptions->setUnknownInvalid( $unknownInvalid );

		$processor = Processor::newFromOptions( $processorOptions );

		$processor->setParameters( $params, $paramDefinitions );

		return $processor;
	}

}
