<commit_before><?php declare(strict_types = 1);

namespace Nextras\Orm\Collection\Helpers;


use Nette\Utils\Arrays;
use Nextras\Dbal\Platforms\Data\Column;
use Nextras\Dbal\QueryBuilder\QueryBuilder;
use Nextras\Orm\Collection\Functions\IQueryBuilderFunction;
use Nextras\Orm\Collection\ICollection;
use Nextras\Orm\Entity\Embeddable\EmbeddableContainer;
use Nextras\Orm\Entity\Reflection\EntityMetadata;
use Nextras\Orm\Entity\Reflection\PropertyMetadata;
use Nextras\Orm\Entity\Reflection\PropertyRelationshipMetadata as Relationship;
use Nextras\Orm\InvalidArgumentException;
use Nextras\Orm\Mapper\Dbal\Conventions\IConventions;
use Nextras\Orm\Mapper\Dbal\DbalMapper;
use Nextras\Orm\Model\IModel;
use Nextras\Orm\NotSupportedException;
use Nextras\Orm\Repository\IRepository;
use function array_map;
use function array_merge;
use function array_pop;
use function array_shift;
use function array_slice;
use function assert;
use function count;
use function implode;
use function is_array;
use function preg_match;
use function reset;


/**
 * QueryBuilder helper for Nextras Dbal.
 */
class DbalQueryBuilderHelper
{
	/** @var IModel */
	private $model;

	/** @var IRepository */
	private $repository;

	/** @var DbalMapper */
	private $mapper;

	/** @var string */
	private $platformName;


	/**
	 * Returns suitable table alias, strips db/schema name and prepends expression $tokens as part of the table name.
	 * @phpstan-param array<int, string> $tokens
	 */
	public static function getAlias(string $name, array $tokens = []): string
	{
		if (preg_match('#^([a-z0-9_]+\.){0,2}+([a-z0-9_]+?)$#i', $name, $m)) {
			$name = $m[2];
		}

		if (count($tokens) === 0) {
			return $name;
		} else {
			return implode('_', $tokens) . '_' . $name;
		}
	}


	public function __construct(IModel $model, IRepository $repository, DbalMapper $mapper)
	{
		$this->model = $model;
		$this->repository = $repository;
		$this->mapper = $mapper;
		$this->platformName = $mapper->getDatabasePlatform()->getName();
	}


	/**
	 * @param string|mixed[] $expr
	 */
	public function processPropertyExpr(QueryBuilder $builder, $expr): DbalExpressionResult
	{
		if (is_array($expr)) {
			$function = array_shift($expr);
			$collectionFunction = $this->repository->getCollectionFunction($function);
			if (!$collectionFunction instanceof IQueryBuilderFunction) {
				throw new InvalidArgumentException("Collection function $function has to implement " . IQueryBuilderFunction::class . ' interface.');
			}
			return $collectionFunction->processQueryBuilderExpression($this, $builder, $expr);
		}

		[$tokens, $sourceEntity] = ConditionParserHelper::parsePropertyExpr($expr);
		return $this->processTokens($tokens, $sourceEntity, $builder);
	}


	/**
	 * @phpstan-param array<string, mixed>|array<int|string, mixed>|list<mixed> $expr
	 */
	public function processFilterFunction(QueryBuilder $builder, array $expr): DbalExpressionResult
	{
		$function = isset($expr[0]) ? array_shift($expr) : ICollection::AND;
		$collectionFunction = $this->repository->getCollectionFunction($function);
		if (!$collectionFunction instanceof IQueryBuilderFunction) {
			throw new InvalidArgumentException("Collection function $function has to implement " . IQueryBuilderFunction::class . ' interface.');
		}
		return $collectionFunction->processQueryBuilderExpression($this, $builder, $expr);
	}


	/**
	 * @param string|array $expr
	 * @phpstan-param string|list<mixed> $expr
	 * @phpstan-return array{string, list<mixed>}
	 */
	public function processOrder(QueryBuilder $builder, $expr, string $direction): array
	{
		$columnReference = $this->processPropertyExpr($builder, $expr);
		return $this->processOrderDirection($columnReference, $direction);
	}


	/**
	 * @phpstan-return array{string, list<mixed>}
	 */
	private function processOrderDirection(DbalExpressionResult $expression, string $direction): array
	{
		if ($this->platformName === 'mysql') {
			if ($direction === ICollection::ASC || $direction === ICollection::ASC_NULLS_FIRST) {
				return ['%ex ASC', $expression->args];
			} elseif ($direction === ICollection::DESC || $direction === ICollection::DESC_NULLS_LAST) {
				return ['%ex DESC', $expression->args];
			} elseif ($direction === ICollection::ASC_NULLS_LAST) {
				return ['%ex IS NULL, %ex ASC', $expression->args, $expression->args];
			} elseif ($direction === ICollection::DESC_NULLS_FIRST) {
				return ['%ex IS NOT NULL, %ex DESC', $expression->args, $expression->args];
			}
		} elseif ($this->platformName === 'mssql') {
			if ($direction === ICollection::ASC || $direction === ICollection::ASC_NULLS_FIRST) {
				return ['%ex ASC', $expression->args];
			} elseif ($direction === ICollection::DESC || $direction === ICollection::DESC_NULLS_LAST) {
				return ['%ex DESC', $expression->args];
			} elseif ($direction === ICollection::ASC_NULLS_LAST) {
				return ['CASE WHEN %ex IS NULL THEN 1 ELSE 0 END, %ex ASC', $expression->args, $expression->args];
			} elseif ($direction === ICollection::DESC_NULLS_FIRST) {
				return ['CASE WHEN %ex IS NOT NULL THEN 1 ELSE 0 END, %ex DESC', $expression->args, $expression->args];
			}
		} elseif ($this->platformName === 'pgsql') {
			if ($direction === ICollection::ASC || $direction === ICollection::ASC_NULLS_LAST) {
				return ['%ex ASC', $expression->args];
			} elseif ($direction === ICollection::DESC || $direction === ICollection::DESC_NULLS_FIRST) {
				return ['%ex DESC', $expression->args];
			} elseif ($direction === ICollection::ASC_NULLS_FIRST) {
				return ['%ex ASC NULLS FIRST', $expression->args];
			} elseif ($direction === ICollection::DESC_NULLS_LAST) {
				return ['%ex DESC NULLS LAST', $expression->args];
			}
		}

		throw new NotSupportedException();
	}


	/**
	 * @param mixed $value
	 * @return mixed
	 */
	public function normalizeValue($value, PropertyMetadata $propertyMetadata, IConventions $conventions)
	{
		if (isset($propertyMetadata->types['array'])) {
			if (is_array($value) && !is_array(reset($value))) {
				$value = [$value];
			}
			if ($propertyMetadata->isPrimary) {
				foreach ($value as $subValue) {
					if (!Arrays::isList($subValue)) {
						throw new InvalidArgumentException('Composite primary value has to be passed as a list, without array keys.');
					}
				}
			}
		}

		if ($propertyMetadata->wrapper !== null) {
			$property = $propertyMetadata->getWrapperPrototype();
			if (is_array($value)) {
				$value = array_map(function ($subValue) use ($property) {
					return $property->convertToRawValue($subValue);
				}, $value);
			} else {
				$value = $property->convertToRawValue($value);
			}
		}

		$tmp = $conventions->convertEntityToStorage([$propertyMetadata->name => $value]);
		$value = reset($tmp);

		return $value;
	}


	/**
	 * @param array<string> $tokens
	 * @param class-string<\Nextras\Orm\Entity\IEntity>|null $sourceEntity
	 */
	private function processTokens(array $tokens, ?string $sourceEntity, QueryBuilder $builder): DbalExpressionResult
	{
		$lastToken = array_pop($tokens);
		assert($lastToken !== null);

		$currentMapper = $this->mapper;
		$currentAlias = $builder->getFromAlias();
		assert($currentAlias !== null);

		$currentConventions = $currentMapper->getConventions();
		$currentEntityMetadata = $currentMapper->getRepository()->getEntityMetadata($sourceEntity);
		$propertyPrefixTokens = "";
		$makeDistinct = false;

		foreach ($tokens as $tokenIndex => $token) {
			$property = $currentEntityMetadata->getProperty($token);
			if ($property->relationship !== null) {
				[
					$currentAlias,
					$currentConventions,
					$currentEntityMetadata,<commit_msg>collection function: extract retreiving a collection function<commit_after><?php declare(strict_types = 1);

namespace Nextras\Orm\Collection\Helpers;


use Nette\Utils\Arrays;
use Nextras\Dbal\Platforms\Data\Column;
use Nextras\Dbal\QueryBuilder\QueryBuilder;
use Nextras\Orm\Collection\Functions\IQueryBuilderFunction;
use Nextras\Orm\Collection\ICollection;
use Nextras\Orm\Entity\Embeddable\EmbeddableContainer;
use Nextras\Orm\Entity\Reflection\EntityMetadata;
use Nextras\Orm\Entity\Reflection\PropertyMetadata;
use Nextras\Orm\Entity\Reflection\PropertyRelationshipMetadata as Relationship;
use Nextras\Orm\InvalidArgumentException;
use Nextras\Orm\Mapper\Dbal\Conventions\IConventions;
use Nextras\Orm\Mapper\Dbal\DbalMapper;
use Nextras\Orm\Model\IModel;
use Nextras\Orm\NotSupportedException;
use Nextras\Orm\Repository\IRepository;
use function array_map;
use function array_merge;
use function array_pop;
use function array_shift;
use function array_slice;
use function assert;
use function count;
use function implode;
use function is_array;
use function preg_match;
use function reset;


/**
 * QueryBuilder helper for Nextras Dbal.
 */
class DbalQueryBuilderHelper
{
	/** @var IModel */
	private $model;

	/** @var IRepository */
	private $repository;

	/** @var DbalMapper */
	private $mapper;

	/** @var string */
	private $platformName;


	/**
	 * Returns suitable table alias, strips db/schema name and prepends expression $tokens as part of the table name.
	 * @phpstan-param array<int, string> $tokens
	 */
	public static function getAlias(string $name, array $tokens = []): string
	{
		if (preg_match('#^([a-z0-9_]+\.){0,2}+([a-z0-9_]+?)$#i', $name, $m)) {
			$name = $m[2];
		}

		if (count($tokens) === 0) {
			return $name;
		} else {
			return implode('_', $tokens) . '_' . $name;
		}
	}


	public function __construct(IModel $model, IRepository $repository, DbalMapper $mapper)
	{
		$this->model = $model;
		$this->repository = $repository;
		$this->mapper = $mapper;
		$this->platformName = $mapper->getDatabasePlatform()->getName();
	}


	/**
	 * @param string|mixed[] $expr
	 */
	public function processPropertyExpr(QueryBuilder $builder, $expr): DbalExpressionResult
	{
		if (is_array($expr)) {
			$function = array_shift($expr);
			$collectionFunction = $this->getCollectionFunction($function);
			return $collectionFunction->processQueryBuilderExpression($this, $builder, $expr);
		}

		[$tokens, $sourceEntity] = ConditionParserHelper::parsePropertyExpr($expr);
		return $this->processTokens($tokens, $sourceEntity, $builder);
	}


	/**
	 * @phpstan-param array<string, mixed>|array<int|string, mixed>|list<mixed> $expr
	 */
	public function processFilterFunction(QueryBuilder $builder, array $expr): DbalExpressionResult
	{
		$function = isset($expr[0]) ? array_shift($expr) : ICollection::AND;
		$collectionFunction = $this->getCollectionFunction($function);
		return $collectionFunction->processQueryBuilderExpression($this, $builder, $expr);
	}


	/**
	 * @param string|array $expr
	 * @phpstan-param string|list<mixed> $expr
	 * @phpstan-return array{string, list<mixed>}
	 */
	public function processOrder(QueryBuilder $builder, $expr, string $direction): array
	{
		$columnReference = $this->processPropertyExpr($builder, $expr);
		return $this->processOrderDirection($columnReference, $direction);
	}


	/**
	 * @phpstan-return array{string, list<mixed>}
	 */
	private function processOrderDirection(DbalExpressionResult $expression, string $direction): array
	{
		if ($this->platformName === 'mysql') {
			if ($direction === ICollection::ASC || $direction === ICollection::ASC_NULLS_FIRST) {
				return ['%ex ASC', $expression->args];
			} elseif ($direction === ICollection::DESC || $direction === ICollection::DESC_NULLS_LAST) {
				return ['%ex DESC', $expression->args];
			} elseif ($direction === ICollection::ASC_NULLS_LAST) {
				return ['%ex IS NULL, %ex ASC', $expression->args, $expression->args];
			} elseif ($direction === ICollection::DESC_NULLS_FIRST) {
				return ['%ex IS NOT NULL, %ex DESC', $expression->args, $expression->args];
			}
		} elseif ($this->platformName === 'mssql') {
			if ($direction === ICollection::ASC || $direction === ICollection::ASC_NULLS_FIRST) {
				return ['%ex ASC', $expression->args];
			} elseif ($direction === ICollection::DESC || $direction === ICollection::DESC_NULLS_LAST) {
				return ['%ex DESC', $expression->args];
			} elseif ($direction === ICollection::ASC_NULLS_LAST) {
				return ['CASE WHEN %ex IS NULL THEN 1 ELSE 0 END, %ex ASC', $expression->args, $expression->args];
			} elseif ($direction === ICollection::DESC_NULLS_FIRST) {
				return ['CASE WHEN %ex IS NOT NULL THEN 1 ELSE 0 END, %ex DESC', $expression->args, $expression->args];
			}
		} elseif ($this->platformName === 'pgsql') {
			if ($direction === ICollection::ASC || $direction === ICollection::ASC_NULLS_LAST) {
				return ['%ex ASC', $expression->args];
			} elseif ($direction === ICollection::DESC || $direction === ICollection::DESC_NULLS_FIRST) {
				return ['%ex DESC', $expression->args];
			} elseif ($direction === ICollection::ASC_NULLS_FIRST) {
				return ['%ex ASC NULLS FIRST', $expression->args];
			} elseif ($direction === ICollection::DESC_NULLS_LAST) {
				return ['%ex DESC NULLS LAST', $expression->args];
			}
		}

		throw new NotSupportedException();
	}


	/**
	 * @param mixed $value
	 * @return mixed
	 */
	public function normalizeValue($value, PropertyMetadata $propertyMetadata, IConventions $conventions)
	{
		if (isset($propertyMetadata->types['array'])) {
			if (is_array($value) && !is_array(reset($value))) {
				$value = [$value];
			}
			if ($propertyMetadata->isPrimary) {
				foreach ($value as $subValue) {
					if (!Arrays::isList($subValue)) {
						throw new InvalidArgumentException('Composite primary value has to be passed as a list, without array keys.');
					}
				}
			}
		}

		if ($propertyMetadata->wrapper !== null) {
			$property = $propertyMetadata->getWrapperPrototype();
			if (is_array($value)) {
				$value = array_map(function ($subValue) use ($property) {
					return $property->convertToRawValue($subValue);
				}, $value);
			} else {
				$value = $property->convertToRawValue($value);
			}
		}

		$tmp = $conventions->convertEntityToStorage([$propertyMetadata->name => $value]);
		$value = reset($tmp);

		return $value;
	}


	private function getCollectionFunction(string $name): IQueryBuilderFunction
	{
		$collectionFunction = $this->repository->getCollectionFunction($name);
		if (!$collectionFunction instanceof IQueryBuilderFunction) {
			throw new InvalidArgumentException("Collection function $name has to implement " . IQueryBuilderFunction::class . ' interface.');
		}
		return $collectionFunction;
	}


	/**
	 * @param array<string> $tokens
	 * @param class-string<\Nextras\Orm\Entity\IEntity>|null $sourceEntity
	 */
	private function processTokens(array $tokens, ?string $sourceEntity, QueryBuilder $builder): DbalExpressionResult
	{
		$lastToken = array_pop($tokens);
		assert($lastToken !== null);

		$currentMapper = $this->mapper;
		$currentAlias = $builder->getFromAlias();
		assert($currentAlias !== null);

		$currentConventions = $currentMapper->getConventions();
		$currentEntityMetadata = $currentMapper->getRepository()->getEntityMetadata($sourceEntity);
		$propertyPrefixTokens = "";
		$makeDistinct = false;

		foreach ($tokens as $tokenIndex => $token) {
			$property = $currentEntityMetadata->getProperty($token);
			if ($property->relationship !== null) {
				[
					$currentAlias,
					$currentConventions,
					$currentEntityMetadata,
{'fraction_non_alphanumeric': 0.1531934486614484, 'fraction_numerical': 0.001841741761494442, 'mean_word_length': 4.955346650998825, 'pattern_counts': {'":': 0, '<': 23, '<?xml version=': 0, '>': 123, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5622249', 'n_tokens_mistral': 5120, 'n_tokens_neox': 4559, 'n_words': 1038}