<?php

namespace CirrusSearch\Query;

use CirrusSearch\CrossSearchStrategy;
use CirrusSearch\Parser\AST\KeywordFeatureNode;
use CirrusSearch\Query\Builder\QueryBuildingContext;
use CirrusSearch\Search\SearchContext;
use CirrusSearch\WarningCollector;
use Elastica\Query;
use Elastica\Query\AbstractQuery;
use Wikimedia\Assert\Assert;

/**
 * File features:
 *  filebits:16  - bit depth
 *  filesize:>300 - size >= 300 kb
 *  filew:100,300 - search of 100 <= file_width <= 300
 * Selects only files of these specified features.
 */
class IndexedNumericFieldFeature extends SimpleKeywordFeature implements FilterQueryFeature {
	/**
	 * Map from feature names to keys
	 * @var string[]
	 */
	private const KEY_TABLE = [
		'filesize' => 'file_size',
		'filebits' => 'file_bits',
		'fileh' => 'file_height',
		'filew' => 'file_width',
		'fileheight' => 'file_height',
		'filewidth' => 'file_width',
		'fileres' => 'file_resolution',
		'textbytes' => 'text_bytes'
	];

	/**
	 * @return string[]
	 */
	protected function getKeywords() {
		return array_keys( self::KEY_TABLE );
	}

	/**
	 * @param KeywordFeatureNode $node
	 * @return CrossSearchStrategy
	 */
	public function getCrossSearchStrategy( KeywordFeatureNode $node ) {
		return CrossSearchStrategy::allWikisStrategy();
	}

	/**
	 * @param SearchContext $context
	 * @param string $key The keyword
	 * @param string $value The value attached to the keyword with quotes stripped
	 * @param string $quotedValue The original value in the search string, including quotes
	 *     if used
	 * @param bool $negated Is the search negated? Not used to generate the returned
	 *     AbstractQuery, that will be negated as necessary. Used for any other building/context
	 *     necessary.
	 * @return array Two element array, first an AbstractQuery or null to apply to the
	 *  query. Second a boolean indicating if the quotedValue should be kept in the search
	 *  string.
	 */
	protected function doApply( SearchContext $context, $key, $value, $quotedValue, $negated ) {
		$query = $this->doGetFilterQuery( $key,
			$this->parseValue( $key, $value, $quotedValue, '', '', $context ) );
		if ( $query === null ) {
			$context->setResultsPossible( false );
		}

		return [ $query, false ];
	}

	/**
	 * @param string $key
	 * @param string $value
	 * @param string $quotedValue
	 * @param string $valueDelimiter
	 * @param string $suffix
	 * @param WarningCollector $warningCollector
	 * @return array|false|null
	 */
	public function parseValue( $key, $value, $quotedValue, $valueDelimiter, $suffix, WarningCollector $warningCollector ) {
		$parsedValue = [];

		$field = self::KEY_TABLE[$key];
		$parsedValue['field'] = $field;
		[ $sign, $number ] = $this->extractSign( $value );
		// filesize treats no sign as >, since exact file size matches make no sense
		if ( !$sign && $key === 'filesize' && strpos( $number, ',' ) === false ) {
			$sign = 1;
		}

		$parsedValue['sign'] = $sign;

		if ( $sign && strpos( $number, ',' ) !== false ) {
			$warningCollector->addWarning(
				'cirrussearch-file-numeric-feature-multi-argument-w-sign',
				$key,
				$number
			);
			return null;
		} elseif ( $sign || strpos( $number, ',' ) === false ) {
			if ( !is_numeric( $number ) ) {
				$this->nanWarning( $warningCollector, $key, $number === '' ? $value : $number );
				return null;
			}
			$parsedValue['value'] = intval( $number );
		} else {
			$numbers = explode( ',', $number, 2 );
			$valid = true;
			if ( !is_numeric( $numbers[0] ) ) {
				$this->nanWarning( $warningCollector, $key, $numbers[0] );
				$valid = false;
			}

			if ( !is_numeric( $numbers[1] ) ) {
				$this->nanWarning( $warningCollector, $key, $numbers[1] );
				$valid = false;
			}
			if ( !$valid ) {
				return null;
			}
			$parsedValue['range'] = [ intval( $numbers[0] ), intval( $numbers[1] ) ];
		}

		return $parsedValue;
	}

	/**
	 * Extract sign prefix which can be < or > or nothing.
	 * @param string $value
	 * @param int $default
	 * @return array Two element array, first the sign: 0 is equal, 1 is more, -1 is less,
	 *  then the number to be compared.
	 */
	protected function extractSign( $value, $default = 0 ) {
		if ( $value[0] == '>' || $value[0] == '<' ) {
			$sign = ( $value[0] == '>' ) ? 1 : -1;
			return [ $sign, substr( $value, 1 ) ];
		} else {
			return [ $default, $value ];
		}
	}

	/**
	 * Adds a warning to the search context that the $key keyword
	 * was provided with the invalid value $notANumber.
	 *
	 * @param WarningCollector $warningCollector
	 * @param string $key
	 * @param string $notANumber
	 */
	protected function nanWarning( WarningCollector $warningCollector, $key, $notANumber ) {
		$warningCollector->addWarning(
			'cirrussearch-file-numeric-feature-not-a-number',
			$key,
			$notANumber
		);
	}

	/**
	 * @param string $field
	 * @param int $from
	 * @param int $to
	 * @param int $multiplier
	 * @return Query\AbstractQuery
	 */
	private function buildBoundedIntervalQuery( $field, $from, $to, $multiplier = 1 ) {
		return new Query\Range( $field, [
			'gte' => $from * $multiplier,
			'lte' => $to * $multiplier
		] );
	}

	/**
	 * @param string $field
	 * @param int $sign
	 * @param int $value
	 * @param int $multiplier
	 * @return Query\AbstractQuery
	 */
	private function buildIntervalQuery( $field, $sign, $value, $multiplier = 1 ) {
		Assert::parameter( $sign != 0, 'sign', 'sign must be non zero' );
		if ( $sign > 0 ) {
			$range = [ 'gte' => $value * $multiplier ];
		} else {
			$range = [ 'lte' => $value * $multiplier ];
		}
		return new Query\Range( $field, $range );
	}

	/**
	 * @param string $field
	 * @param int $value
	 * @param int $multiplier
	 * @return Query\AbstractQuery
	 */
	private function buildMatchQuery( $field, $value, $multiplier = 1 ) {
		$query = new Query\MatchQuery();
		$query->setFieldQuery( $field, (string)( $value * $multiplier ) );
		return $query;
	}

	/**
	 * @param string $key
	 * @param array $parsedValue
	 * @return Query\AbstractQuery|null
	 */
	protected function doGetFilterQuery( $key, $parsedValue ) {
		if ( $parsedValue === null ) {
			return null;
		}
		$field = $parsedValue['field'];
		$sign = $parsedValue['sign'];
		$multiplier = ( $key === 'filesize' ) ? 1024 : 1;

		if ( isset( $parsedValue['range'] ) ) {
			$query =
				$this->buildBoundedIntervalQuery( $parsedValue['field'], $parsedValue['range'][0],
					$parsedValue['range'][1], $multiplier );
		} elseif ( $sign === 0 ) {
			$query = $this->buildMatchQuery( $field, $parsedValue['value'], $multiplier );
		} else {
			$query = $this->buildIntervalQuery( $field, $sign, $parsedValue['value'], $multiplier );
		}

		return $query;
	}

	/**
	 * @param KeywordFeatureNode $node
	 * @param QueryBuildingContext $context
	 * @return AbstractQuery|null
	 */
	public function getFilterQuery( KeywordFeatureNode $node, QueryBuildingContext $context ) {
		return $this->doGetFilterQuery( $node->getKey(), $node->getParsedValue() );
	}
}
