<?php

declare(strict_types=1);


/**
 * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */


namespace OCA\Circles\Db;

use Doctrine\DBAL\Query\QueryBuilder;
use OC;
use OCA\Circles\Exceptions\RequestBuilderException;
use OCA\Circles\IFederatedModel;
use OCA\Circles\IFederatedUser;
use OCA\Circles\Model\Circle;
use OCA\Circles\Model\Federated\RemoteInstance;
use OCA\Circles\Model\FederatedUser;
use OCA\Circles\Model\Member;
use OCA\Circles\Model\Probes\CircleProbe;
use OCA\Circles\Service\ConfigService;
use OCA\Circles\Tools\Db\ExtendedQueryBuilder;
use OCA\Circles\Tools\Traits\TArrayTools;
use OCP\DB\QueryBuilder\ICompositeExpression;
use OCP\DB\QueryBuilder\IQueryBuilder;

/**
 * Class CoreQueryBuilder
 *
 * @package OCA\Circles\Db
 */
class CoreQueryBuilder extends ExtendedQueryBuilder {
	use TArrayTools;


	public const SINGLE = 'a';
	public const CIRCLE = 'b';
	public const MEMBER = 'c';
	public const OWNER = 'd';
	public const FEDERATED_EVENT = 'e';
	public const REMOTE = 'f';
	public const BASED_ON = 'g';
	public const INITIATOR = 'h';
	public const DIRECT_INITIATOR = 'i';
	public const MEMBERSHIPS = 'j';
	public const CONFIG = 'k';
	public const UPSTREAM_MEMBERSHIPS = 'l';
	public const INHERITANCE_FROM = 'm';
	public const INHERITED_BY = 'n';
	public const INVITED_BY = 'o';
	public const MOUNT = 'p';
	public const MOUNTPOINT = 'q';
	public const SHARE = 'r';
	public const FILE_CACHE = 's';
	public const STORAGES = 't';
	public const TOKEN = 'u';
	public const OPTIONS = 'v';
	public const HELPER = 'w';


	public static $SQL_PATH = [
		self::SINGLE => [
			self::MEMBER
		],
		self::CIRCLE => [
			self::OPTIONS => [
			],
			self::MEMBER,
			self::OWNER => [
				self::BASED_ON
			],
			self::MEMBERSHIPS => [
				self::CONFIG
			],
			self::DIRECT_INITIATOR => [
				self::BASED_ON
			],
			self::INITIATOR => [
				self::BASED_ON,
				self::INHERITED_BY => [
					self::MEMBERSHIPS
				]
			],
			self::REMOTE => [
				self::MEMBER,
				self::CIRCLE => [
					self::OWNER
				]
			]
		],
		self::MEMBER => [
			self::MEMBERSHIPS => [
				self::CONFIG
			],
			self::INHERITANCE_FROM,
			self::CIRCLE => [
				self::OPTIONS => [
					'getData' => true
				],
				self::OWNER,
				self::MEMBERSHIPS => [
					self::CONFIG
				],
				self::DIRECT_INITIATOR,
				self::INITIATOR => [
					self::OPTIONS => [
						'minimumLevel' => Member::LEVEL_MEMBER
					],
					self::BASED_ON,
					self::INHERITED_BY => [
						self::MEMBERSHIPS
					],
					self::INVITED_BY => [
						self::OWNER,
						self::BASED_ON
					]
				]
			],
			self::BASED_ON => [
				self::OWNER,
				self::MEMBERSHIPS,
				self::INITIATOR => [
					self::BASED_ON,
					self::INHERITED_BY => [
						self::MEMBERSHIPS
					]
				]
			],
			self::REMOTE => [
				self::MEMBER,
				self::CIRCLE => [
					self::OWNER
				]
			],
			self::INVITED_BY => [
				self::OWNER,
				self::BASED_ON
			]
		],
		self::MEMBERSHIPS => [
			self::CONFIG
		],
		self::SHARE => [
			self::SHARE,
			self::TOKEN,
			self::FILE_CACHE => [
				self::STORAGES
			],
			self::UPSTREAM_MEMBERSHIPS => [
				self::MEMBERSHIPS,
				self::INHERITED_BY => [
					self::BASED_ON
				],
				self::SHARE,
			],
			self::MEMBERSHIPS => [
				self::CONFIG
			],
			self::INHERITANCE_FROM,
			self::INHERITED_BY => [
				self::BASED_ON
			],
			self::CIRCLE => [
				self::OWNER
			],
			self::INITIATOR => [
				self::BASED_ON,
				self::INHERITED_BY => [
					self::MEMBERSHIPS
				]
			]
		],
		self::REMOTE => [
			self::MEMBER
		],
		self::MOUNT => [
			self::MEMBER => [
				self::REMOTE
			],
			self::INITIATOR => [
				self::INHERITED_BY => [
					self::MEMBERSHIPS
				]
			],
			self::MOUNTPOINT,
			self::MEMBERSHIPS => [
				self::CONFIG
			]
		],
		self::HELPER => [
			self::MEMBERSHIPS => [
				self::CONFIG
			],
			self::INITIATOR => [
				self::INHERITED_BY => [
					self::MEMBERSHIPS
				]
			],
			self::CIRCLE => [
				self::OPTIONS => [
				],
				self::MEMBER,
				self::OWNER => [
					self::BASED_ON
				]
			]
		]
	];


	/** @var ConfigService */
	private $configService;

	private array $options = [];
	private array $sqlPath = [];

	/**
	 * CoreQueryBuilder constructor.
	 */
	public function __construct() {
		parent::__construct();

		$this->configService = OC::$server->get(ConfigService::class);
	}


	/**
	 * @param IFederatedModel $federatedModel
	 *
	 * @return string
	 */
	public function getInstance(IFederatedModel $federatedModel): string {
		$instance = $federatedModel->getInstance();

		return ($this->configService->isLocalInstance($instance)) ? '' : $instance;
	}


	/**
	 * @param string $id
	 */
	public function limitToCircleId(string $id): void {
		$this->limit('circle_id', $id, '', true);
	}

	/**
	 * @param string $name
	 */
	public function limitToName(string $name): void {
		$this->limit('name', $name);
	}

	/**
	 * @param string $name
	 */
	public function limitToDisplayName(string $name): void {
		$this->limit('display_name', $name, '', false);
	}

	/**
	 * @param string $name
	 */
	public function limitToSanitizedName(string $name): void {
		$this->limit('sanitized_name', $name, '', false);
	}

	/**
	 * @param int $config
	 */
	public function limitToConfig(int $config): void {
		$this->limitInt('config', $config);
	}

	/**
	 * @param int $source
	 */
	public function limitToSource(int $source): void {
		$this->limitInt('source', $source);
	}

	/**
	 * @param int $config
	 * @param string $alias
	 */
	public function limitToConfigFlag(int $config, string $alias = ''): void {
		$this->limitBitwise('config', $config, $alias);
	}


	/**
	 * @param string $singleId
	 */
	public function limitToSingleId(string $singleId, string $alias = ''): void {
		$this->limit('single_id', $singleId, $alias, true);
	}


	/**
	 * @param string $itemId
	 */
	public function limitToItemId(string $itemId): void {
		$this->limit('item_id', $itemId, '', true);
	}


	/**
	 * @param string $host
	 */
	public function limitToInstance(string $host): void {
		$this->limit('instance', $host, '', false);
	}


	/**
	 * @param int $userType
	 */
	public function limitToUserType(int $userType): void {
		$this->limitInt('user_type', $userType);
	}


	/**
	 * @param int $shareType
	 */
	public function limitToShareType(int $shareType): void {
		$this->limitInt('share_type', $shareType);
	}


	/**
	 * @param string $shareWith
	 */
	public function limitToShareWith(string $shareWith): void {
		$this->limit('share_with', $shareWith, '', true);
	}


	/**
	 * @param int $nodeId
	 */
	public function limitToFileSource(int $nodeId): void {
		$this->limitInt('file_source', $nodeId);
	}

	/**
	 * @param array $files
	 */
	public function limitToFileSourceArray(array $files): void {
		$this->limitInArray('file_source', $files, type: IQueryBuilder::PARAM_INT_ARRAY);
	}


	/**
	 * @param int $shareId
	 */
	public function limitToShareParent(int $shareId): void {
		$this->limitInt('parent', $shareId);
	}


	/**
	 * filter result on details (ie. displayName, Description, ...)
	 *
	 * @param Circle $circle
	 */
	public function filterCircleDetails(Circle $circle): void {
		if ($this->getType() !== QueryBuilder::SELECT) {
			return;
		}

		$expr = $this->expr();
		$orX = $expr->orX();
		if ($circle->getDisplayName() !== '') {
			$andX = $expr->andX();
			foreach (explode(' ', $circle->getDisplayName()) as $word) {
				$andX->add(
					$expr->iLike(
						$this->getDefaultSelectAlias() . '.' . 'display_name',
						$this->createNamedParameter('%' . $word . '%')
					)
				);
			}
			$orX->add($andX);
		}

		if ($orX->count() > 0) {
			$this->andWhere($orX);
		}

		if ($circle->getSource() > 0) {
			$this->limitInt('source', $circle->getSource());
		}
		if ($circle->getConfig() > 0) {
			$this->limitBitwise('config', $circle->getConfig());
		}
	}


	/**
	 * left join RemoteInstance based on a Member
	 */
	public function leftJoinRemoteInstance(string $alias): void {
		$expr = $this->expr();

		try {
			$aliasRemoteInstance = $this->generateAlias($alias, self::REMOTE);
			$this->generateRemoteInstanceSelectAlias($aliasRemoteInstance)
				 ->leftJoin(
				 	$alias, CoreRequestBuilder::TABLE_REMOTE, $aliasRemoteInstance,
				 	$expr->eq($alias . '.instance', $aliasRemoteInstance . '.instance')
				 );
		} catch (RequestBuilderException $e) {
		}
	}


	/**
	 * @param string $alias
	 * @param RemoteInstance $remoteInstance
	 * @param bool $filterSensitiveData
	 * @param string $aliasCircle
	 *
	 * @throws RequestBuilderException
	 */
	public function limitToRemoteInstance(
		string $alias,
		RemoteInstance $remoteInstance,
		bool $filterSensitiveData = true,
		string $aliasCircle = ''
	): void {
		if ($aliasCircle === '') {
			$aliasCircle = $alias;
		}

		$this->leftJoinRemoteInstanceIncomingRequest($alias, $remoteInstance);
		$this->leftJoinMemberFromInstance($alias, $remoteInstance, $aliasCircle);
		$this->leftJoinMemberFromRemoteCircle($alias, $remoteInstance, $aliasCircle);
		$this->limitRemoteVisibility($alias, $filterSensitiveData, $aliasCircle);
	}


	/**
	 * Left join RemoteInstance based on an incoming request
	 *
	 * @param string $alias
	 * @param RemoteInstance $remoteInstance
	 *
	 * @throws RequestBuilderException
	 */
	public function leftJoinRemoteInstanceIncomingRequest(
		string $alias,
		RemoteInstance $remoteInstance
	): void {
		if ($this->getType() !== QueryBuilder::SELECT) {
			return;
		}

		$aliasRemote = $this->generateAlias($alias, self::REMOTE);
		$expr = $this->expr();
		$this->leftJoin(
			$this->getDefaultSelectAlias(), CoreRequestBuilder::TABLE_REMOTE, $aliasRemote,
			$expr->eq($aliasRemote . '.instance', $this->createNamedParameter($remoteInstance->getInstance()))
		);
	}


	/**
	 * left join members to check memberships of someone from instance
	 *
	 * @param string $alias
	 * @param RemoteInstance $remoteInstance
	 * @param string $aliasCircle
	 *
	 * @throws RequestBuilderException
	 */
	private function leftJoinMemberFromInstance(
		string $alias, RemoteInstance $remoteInstance, string $aliasCircle
	) {
		if ($this->getType() !== QueryBuilder::SELECT) {
			return;
		}

		$aliasRemote = $this->generateAlias($alias, self::REMOTE);
		$aliasRemoteMember = $this->generateAlias($aliasRemote, self::MEMBER);

		$expr = $this->expr();
		$this->leftJoin(
			$this->getDefaultSelectAlias(), CoreRequestBuilder::TABLE_MEMBER, $aliasRemoteMember,
			$expr->andX(
				$expr->eq($aliasRemoteMember . '.circle_id', $aliasCircle . '.unique_id'),
				$expr->eq(
					$aliasRemoteMember . '.instance',
					$this->createNamedParameter($remoteInstance->getInstance())
				),
				$expr->gte($aliasRemoteMember . '.level', $this->createNamedParameter(Member::LEVEL_MEMBER))
			)
		);
	}


	/**
	 * left join circle is member of a circle from remote instance
	 *
	 * @param string $alias
	 * @param RemoteInstance $remoteInstance
	 * @param string $aliasCircle
	 *
	 * @throws RequestBuilderException
	 */
	private function leftJoinMemberFromRemoteCircle(
		string $alias,
		RemoteInstance $remoteInstance,
		string $aliasCircle
	) {
		if ($this->getType() !== QueryBuilder::SELECT) {
			return;
		}

		$aliasRemote = $this->generateAlias($alias, self::REMOTE);
		$aliasRemoteCircle = $this->generateAlias($aliasRemote, self::CIRCLE);
		$aliasRemoteCircleOwner = $this->generateAlias($aliasRemoteCircle, self::OWNER);

		$expr = $this->expr();
		$this->leftJoin(
			$this->getDefaultSelectAlias(), CoreRequestBuilder::TABLE_MEMBER, $aliasRemoteCircle,
			$expr->andX(
				$expr->eq($aliasRemoteCircle . '.single_id', $aliasCircle . '.unique_id'),
				$expr->emptyString($aliasRemoteCircle . '.instance'),
				$expr->gte($aliasRemoteCircle . '.level', $this->createNamedParameter(Member::LEVEL_MEMBER))
			)
		);
		$this->leftJoin(
			$this->getDefaultSelectAlias(), CoreRequestBuilder::TABLE_MEMBER, $aliasRemoteCircleOwner,
			$expr->andX(
				$expr->eq($aliasRemoteCircle . '.circle_id', $aliasRemoteCircleOwner . '.circle_id'),
				$expr->eq(
					$aliasRemoteCircleOwner . '.instance',
					$this->createNamedParameter($remoteInstance->getInstance())
				),
				$expr->eq(
					$aliasRemoteCircleOwner . '.level', $this->createNamedParameter(Member::LEVEL_OWNER)
				)
			)
		);
	}


	/**
	 * - global_scale: visibility on all Circles
	 * - trusted: visibility on all FEDERATED Circle if owner is local
	 * - external: visibility on all FEDERATED Circle if owner is local and:
	 *    - with if Circle contains at least one member from the remote instance
	 *    - one circle from the remote instance contains the local circle as member, and confirmed (using
	 *      sync locally)
	 * - passive: like external, but the members list will only contains member from the local instance and
	 * from the remote instance.
	 *
	 * @param string $alias
	 * @param bool $sensitive
	 * @param string $aliasCircle
	 *
	 * @throws RequestBuilderException
	 */
	protected function limitRemoteVisibility(string $alias, bool $sensitive, string $aliasCircle) {
		$aliasRemote = $this->generateAlias($alias, self::REMOTE);
		$aliasOwner = $this->generateAlias($aliasCircle, self::OWNER);
		$aliasRemoteMember = $this->generateAlias($aliasRemote, self::MEMBER);
		$aliasRemoteCircle = $this->generateAlias($aliasRemote, self::CIRCLE);
		$aliasRemoteCircleOwner = $this->generateAlias($aliasRemoteCircle, self::OWNER);

		$expr = $this->expr();
		$orX = $expr->orX();
		$orX->add(
			$expr->eq($aliasRemote . '.type', $this->createNamedParameter(RemoteInstance::TYPE_GLOBALSCALE))
		);

		$orExtOrPassive = $expr->orX();
		$orExtOrPassive->add(
			$expr->eq($aliasRemote . '.type', $this->createNamedParameter(RemoteInstance::TYPE_EXTERNAL))
		);
		if (!$sensitive) {
			$orExtOrPassive->add(
				$expr->eq($aliasRemote . '.type', $this->createNamedParameter(RemoteInstance::TYPE_PASSIVE))
			);
		} else {
			if ($this->getDefaultSelectAlias() === CoreQueryBuilder::MEMBER) {
				$orExtOrPassive->add($this->limitRemoteVisibility_Sensitive_Members($aliasRemote));
			}
		}

		$orInstance = $expr->orX();
		$orInstance->add($expr->isNotNull($aliasRemoteMember . '.instance'));
		$orInstance->add($expr->isNotNull($aliasRemoteCircleOwner . '.instance'));

		$andExternal = $expr->andX();
		$andExternal->add($orExtOrPassive);
		$andExternal->add($orInstance);

		$orExtOrTrusted = $expr->orX();
		$orExtOrTrusted->add($andExternal);
		$orExtOrTrusted->add(
			$expr->eq($aliasRemote . '.type', $this->createNamedParameter(RemoteInstance::TYPE_TRUSTED))
		);

		$andTrusted = $expr->andX();
		$andTrusted->add($orExtOrTrusted);
		$andTrusted->add($this->exprLimitBitwise('config', Circle::CFG_FEDERATED, $aliasCircle));
		$andTrusted->add($expr->emptyString($aliasOwner . '.instance'));
		$orX->add($andTrusted);

		$this->andWhere($orX);
	}


	/**
	 * @param string $alias
	 * @param Member $member
	 *
	 * @throws RequestBuilderException
	 */
	public function limitToDirectMembership(string $alias, Member $member): void {
		if ($this->getType() !== QueryBuilder::SELECT) {
			return;
		}

		$aliasMember = $this->generateAlias($alias, self::MEMBER, $options);
		$getData = $this->getBool('getData', $options, false);

		$expr = $this->expr();
		if ($getData) {
			$this->generateMemberSelectAlias($aliasMember);
		}
		$this->innerJoin(
			$this->getDefaultSelectAlias(), CoreRequestBuilder::TABLE_MEMBER, $aliasMember,
			$expr->eq($aliasMember . '.circle_id', $alias . '.unique_id')
		);

		$this->filterDirectMembership($aliasMember, $member);
	}


	/**
	 * @param string $alias
	 * @param string $aliasCircle
	 * @param FederatedUser $federatedUser
	 *
	 * @throws RequestBuilderException
	 */
	public function limitToFederatedUserMemberships(
		string $alias,
		string $aliasCircle,
		FederatedUser $federatedUser
	): void {
		$aliasMembership = $this->generateAlias($alias, self::MEMBERSHIPS);

		$this->leftJoin(
			$aliasCircle,
			'circles_membership',
			$aliasMembership,
			$this->expr()->eq($aliasCircle . '.unique_id', $aliasMembership . '.circle_id')
		);

		$this->andWhere(
			$this->expr()->eq(
				$aliasMembership . '.single_id',
				$this->createNamedParameter($federatedUser->getSingleId())
			)
		);
	}


	/**
	 * @param string $aliasMember
	 * @param Member $member
	 */
	public function filterDirectMembership(string $aliasMember, Member $member): void {
		if ($this->getType() !== QueryBuilder::SELECT) {
			return;
		}

		$expr = $this->expr();
		$andX = $expr->andX();

		if ($member->getUserId() !== '') {
			$andX->add(
				$expr->eq($aliasMember . '.user_id', $this->createNamedParameter($member->getUserId()))
			);
		}

		if ($member->getSingleId() !== '') {
			$andX->add(
				$expr->eq($aliasMember . '.single_id', $this->createNamedParameter($member->getSingleId()))
			);
		}

		if ($member->getUserType() > 0) {
			$andX->add(
				$expr->eq($aliasMember . '.user_type', $this->createNamedParameter($member->getUserType()))
			);
		}

		$this->limitToInstance($this->getInstance($member));

		if ($member->getLevel() > 0) {
			$andX->add(
				$expr->gte(
					$aliasMember . '.level',
					$this->createNamedParameter($member->getLevel(), IQueryBuilder::PARAM_INT)
				)
			);
		}

		$this->andWhere($andX);
	}


	/**
	 * @param string $alias
	 * @param IFederatedUser|null $initiator
	 * @param string $field
	 * @param string $helperAlias
	 *
	 * @throws RequestBuilderException
	 */
	public function leftJoinCircle(
		string $alias,
		?IFederatedUser $initiator = null,
		string $field = 'circle_id',
		string $helperAlias = ''
	): void {
		if ($this->getType() !== QueryBuilder::SELECT) {
			return;
		}

		$helperAlias = ($helperAlias !== '') ? $helperAlias : $alias;
		$aliasCircle = $this->generateAlias($alias, self::CIRCLE, $options);
		$getData = $this->getBool('getData', $options, false);
		$expr = $this->expr();

		if ($getData) {
			$this->generateCircleSelectAlias($aliasCircle);
		}

		$this->leftJoin(
			$helperAlias,
			CoreRequestBuilder::TABLE_CIRCLE,
			$aliasCircle,
			$expr->eq($aliasCircle . '.unique_id', $helperAlias . '.' . $field)
		);

		if (!is_null($initiator)) {
			$this->limitToInitiator($aliasCircle, $initiator);
		}

		$this->leftJoinOwner($aliasCircle);
	}


	/**
	 * @param string $aliasMember
	 *
	 * @throws RequestBuilderException
	 */
	public function leftJoinInvitedBy(string $aliasMember): void {
		if ($this->getType() !== QueryBuilder::SELECT) {
			return;
		}

		try {
			$aliasInvitedBy = $this->generateAlias($aliasMember, self::INVITED_BY);
		} catch (RequestBuilderException $e) {
			return;
		}

		$expr = $this->expr();
		$this->generateCircleSelectAlias($aliasInvitedBy)
			 ->leftJoin(
			 	$aliasMember, CoreRequestBuilder::TABLE_CIRCLE, $aliasInvitedBy,
			 	$expr->eq($aliasMember . '.invited_by', $aliasInvitedBy . '.unique_id')
			 );

		$this->leftJoinOwner($aliasInvitedBy);
	}


	/**
	 * @param string $aliasMember
	 * @param IFederatedUser|null $initiator
	 *
	 * @throws RequestBuilderException
	 */
	public function leftJoinBasedOn(
		string $aliasMember,
		?IFederatedUser $initiator = null
	): void {
		if ($this->getType() !== QueryBuilder::SELECT) {
			return;
		}

		try {
			$aliasBasedOn = $this->generateAlias($aliasMember, self::BASED_ON, $options);
		} catch (RequestBuilderException $e) {
			return;
		}

		$expr = $this->expr();
		$this->generateCircleSelectAlias($aliasBasedOn)
			 ->leftJoin(
			 	$aliasMember, CoreRequestBuilder::TABLE_CIRCLE, $aliasBasedOn,
			 	$expr->eq($aliasBasedOn . '.unique_id', $aliasMember . '.single_id')
			 );

		if (!is_null($initiator)) {
			$this->leftJoinInitiator($aliasBasedOn, $initiator);
			$this->leftJoinOwner($aliasBasedOn);
		}
	}


	/**
	 * @param string $alias
	 * @param string $field
	 *
	 * @throws RequestBuilderException
	 */
	public function leftJoinOwner(string $alias, string $field = 'unique_id'): void {
		if ($this->getType() !== QueryBuilder::SELECT) {
			return;
		}

		try {
			$aliasMember = $this->generateAlias($alias, self::OWNER, $options);
			$getData = $this->getBool('getData', $options, false);
		} catch (RequestBuilderException $e) {
			return;
		}

		$expr = $this->expr();
		$this->generateMemberSelectAlias($aliasMember)
			 ->leftJoin(
			 	$alias, CoreRequestBuilder::TABLE_MEMBER, $aliasMember,
			 	$expr->andX(
			 		$expr->eq($aliasMember . '.circle_id', $alias . '.' . $field),
			 		$expr->eq(
			 			$aliasMember . '.level',
			 			$this->createNamedParameter(Member::LEVEL_OWNER, self::PARAM_INT)
			 		)
			 	)
			 );

		$this->leftJoinBasedOn($aliasMember);
	}


	/**
	 * @param CircleProbe $probe
	 * @param string $alias
	 * @param string $field
	 */
	public function innerJoinMembership(
		CircleProbe $probe,
		string $alias,
		string $field = 'unique_id'
	): void {
		if ($this->getType() !== QueryBuilder::SELECT) {
			return;
		}

		try {
			$aliasMembership = $this->generateAlias($alias, self::MEMBERSHIPS);
		} catch (RequestBuilderException $e) {
			return;
		}

		$expr = $this->expr();

		$on = $expr->andX($expr->eq($aliasMembership . '.circle_id', $alias . '.' . $field));

		// limit on membership level if requested
		$minLevel = $probe->getMinimumLevel();
		if ($minLevel > Member::LEVEL_MEMBER) {
			$on->add($this->exprGt('level', $minLevel, true, $aliasMembership));
		}

		$this->generateMembershipSelectAlias($aliasMembership)
			 ->innerJoin($alias, CoreRequestBuilder::TABLE_MEMBERSHIP, $aliasMembership, $on);
	}


	/**
	 * @param string $alias
	 * @param string $fieldCircleId
	 * @param string $fieldSingleId
	 *
	 * @throws RequestBuilderException
	 */
	public function leftJoinMember(
		string $alias,
		string $fieldCircleId = 'circle_id',
		string $fieldSingleId = 'single_id'
	): void {
		if ($this->getType() !== QueryBuilder::SELECT) {
			return;
		}

		try {
			$aliasMember = $this->generateAlias($alias, self::MEMBER, $options);
			$getData = $this->getBool('getData', $options, false);
		} catch (RequestBuilderException $e) {
			return;
		}

		$expr = $this->expr();
		$this->generateMemberSelectAlias($aliasMember)
			 ->leftJoin(
			 	$alias, CoreRequestBuilder::TABLE_MEMBER, $aliasMember,
			 	$expr->andX(
			 		$expr->eq($aliasMember . '.circle_id', $alias . '.' . $fieldCircleId),
			 		$expr->eq($aliasMember . '.single_id', $alias . '.' . $fieldSingleId),
			 		$this->exprGt('level', Member::LEVEL_MEMBER, true, $aliasMember)
			 	)
			 );

		$this->leftJoinRemoteInstance($aliasMember);
		$this->leftJoinBasedOn($aliasMember);
	}


	/**
	 * if 'getData' is true, will returns 'inheritanceBy': the Member at the end of a sub-chain of
	 * memberships (based on $field for Top Circle's singleId)
	 *
	 * @param string $alias
	 * @param string $field
	 * @param string $aliasInheritedBy
	 *
	 * @throws RequestBuilderException
	 */
	public function leftJoinInheritedMembers(
		string $alias,
		string $field = '',
		string $aliasInheritedBy = ''
	): void {
		$expr = $this->expr();

		$field = ($field === '') ? 'circle_id' : $field;
		$aliasMembership = $this->generateAlias($alias, self::MEMBERSHIPS, $options);

		$this->leftJoin(
			$alias, CoreRequestBuilder::TABLE_MEMBERSHIP, $aliasMembership,
			$expr->eq($aliasMembership . '.circle_id', $alias . '.' . $field)
		);

		//		if (!$this->getBool('getData', $options, false)) {
		//			return;
		//		}

		if ($aliasInheritedBy === '') {
			$aliasInheritedBy = $this->generateAlias($alias, self::INHERITED_BY);
		}
		$this->generateMemberSelectAlias($aliasInheritedBy)
			 ->leftJoin(
			 	$alias, CoreRequestBuilder::TABLE_MEMBER, $aliasInheritedBy,
			 	$expr->andX(
			 		$expr->eq($aliasMembership . '.inheritance_last', $aliasInheritedBy . '.circle_id'),
			 		$expr->eq($aliasMembership . '.single_id', $aliasInheritedBy . '.single_id')
			 	)
			 );

		$this->leftJoinBasedOn($aliasInheritedBy);
	}


	/**
	 * @throws RequestBuilderException
	 */
	public function limitToInheritedMemberships(string $alias, string $singleId, string $field = ''): void {
		$expr = $this->expr();
		$field = ($field === '') ? 'circle_id' : $field;
		$aliasUpstreamMembership = $this->generateAlias($alias, self::UPSTREAM_MEMBERSHIPS, $options);
		$this->leftJoin(
			$alias, CoreRequestBuilder::TABLE_MEMBERSHIP, $aliasUpstreamMembership,
			$expr->eq($aliasUpstreamMembership . '.single_id', $this->createNamedParameter($singleId))
		);

		$orX = $expr->orX(
			$expr->eq($aliasUpstreamMembership . '.circle_id', $alias . '.' . $field),
			$expr->eq($alias . '.' . $field, $this->createNamedParameter($singleId))
		);

		$this->andWhere($orX);
	}


	/**
	 * limit the request to Members and Sub Members of a Circle.
	 *
	 * @param string $alias
	 * @param string $singleId
	 * @param int $level
	 *
	 * @throws RequestBuilderException
	 */
	public function limitToMembersByInheritance(string $alias, string $singleId, int $level = 0): void {
		$this->leftJoinMembersByInheritance($alias);

		$expr = $this->expr();
		$aliasMembership = $this->generateAlias($alias, self::MEMBERSHIPS);
		$this->andWhere($expr->eq($aliasMembership . '.circle_id', $this->createNamedParameter($singleId)));
		if ($level > 1) {
			$this->andWhere(
				$expr->gte(
					$aliasMembership . '.level',
					$this->createNamedParameter($level, IQueryBuilder::PARAM_INT)
				)
			);
		}
	}


	/**
	 * if 'getData' is true, will returns 'inheritanceFrom': the Circle-As-Member of the Top Circle
	 * that explain the membership of a Member (based on $field for singleId) to a specific Circle
	 *
	 * // TODO: returns the link/path ?
	 *
	 * @param string $alias
	 * @param string $field
	 *
	 * @throws RequestBuilderException
	 */
	public function leftJoinMembersByInheritance(string $alias, string $field = ''): void {
		$expr = $this->expr();

		$field = ($field === '') ? 'circle_id' : $field;
		$aliasMembership = $this->generateAlias($alias, self::MEMBERSHIPS, $options);

		$this->leftJoin(
			$alias, CoreRequestBuilder::TABLE_MEMBERSHIP, $aliasMembership,
			$expr->andX(
				$expr->eq($aliasMembership . '.inheritance_last', $alias . '.' . $field),
				$expr->eq($aliasMembership . '.single_id', $alias . '.single_id')
			)
		);

		if (!$this->getBool('getData', $options, false)) {
			return;
		}

		$aliasInheritanceFrom = $this->generateAlias($alias, self::INHERITANCE_FROM);
		$this->generateMemberSelectAlias($aliasInheritanceFrom)
			 ->leftJoin(
			 	$aliasMembership, CoreRequestBuilder::TABLE_MEMBER, $aliasInheritanceFrom,
			 	$expr->andX(
			 		$expr->eq($aliasMembership . '.circle_id', $aliasInheritanceFrom . '.circle_id'),
			 		$expr->eq($aliasMembership . '.inheritance_first', $aliasInheritanceFrom . '.single_id')
			 	)
			 );
	}


	/**
	 * @param string $alias
	 * @param string $token
	 *
	 * @throws RequestBuilderException
	 */
	public function limitToShareToken(string $alias, string $token): void {
		$this->leftJoinShareToken($alias);

		$aliasShareToken = $this->generateAlias($alias, self::TOKEN, $options);
		$this->generateSelectAlias(
			CoreRequestBuilder::$tables[CoreRequestBuilder::TABLE_TOKEN],
			$aliasShareToken,
			$aliasShareToken,
			[]
		);

		$this->limit('token', $token, $aliasShareToken);
	}

	/**
	 * @param string $alias
	 * @param string $field
	 *
	 * @throws RequestBuilderException
	 */
	public function leftJoinShareToken(string $alias, string $field = ''): void {
		$expr = $this->expr();

		$field = ($field === '') ? 'id' : $field;
		$aliasShareToken = $this->generateAlias($alias, self::TOKEN, $options);

		$this->leftJoin(
			$alias, CoreRequestBuilder::TABLE_TOKEN, $aliasShareToken,
			$expr->eq($aliasShareToken . '.share_id', $alias . '.' . $field)
		);
	}


	/**
	 * limit the result to the point of view of a FederatedUser
	 *
	 * @param string $alias
	 * @param IFederatedUser $user
	 * @param string $field
	 * @param string $helperAlias
	 *
	 * @return ICompositeExpression
	 * @throws RequestBuilderException
	 */
	public function limitToInitiator(
		string $alias,
		IFederatedUser $user,
		string $field = '',
		string $helperAlias = ''
	): ICompositeExpression {
		$this->leftJoinInitiator($alias, $user, $field, $helperAlias);
		$where = $this->limitInitiatorVisibility($alias);

		$aliasInitiator = $this->generateAlias($alias, self::INITIATOR, $options);
		if ($this->getBool('getData', $options, false)) {
			$this->leftJoinBasedOn($aliasInitiator);
		}

		return $where;
	}


	/**
	 * @param string $alias
	 */
	public function leftJoinCircleConfig(string $alias): void {
		$expr = $this->expr();
		try {
			$aliasConfig = $this->generateAlias($alias, self::CONFIG, $options);
			$this->selectAlias(
				$aliasConfig . '.config',
				(($alias !== $this->getDefaultSelectAlias()) ? $alias . '_' : '') . 'circle_config'
			);
			$this->leftJoin(
				$alias,
				CoreRequestBuilder::TABLE_CIRCLE,
				$aliasConfig,
				$expr->eq($alias . '.circle_id', $aliasConfig . '.unique_id')
			);
		} catch (RequestBuilderException $e) {
		}
	}


	/**
	 * Left join members to filter userId as initiator.
	 *
	 * @param string $alias
	 * @param IFederatedUser $initiator
	 * @param string $field
	 * @param string $helperAlias
	 *
	 * @throws RequestBuilderException
	 */
	public function leftJoinInitiator(
		string $alias,
		IFederatedUser $initiator,
		string $field = '',
		string $helperAlias = ''
	): void {
		if ($this->getType() !== QueryBuilder::SELECT) {
			return;
		}

		$expr = $this->expr();
		$field = ($field === '') ? 'unique_id' : $field;
		$helperAlias = ($helperAlias !== '') ? $helperAlias : $alias;
		$aliasMembership = $this->generateAlias($alias, self::MEMBERSHIPS, $options);

		$this->leftJoin(
			$helperAlias,
			CoreRequestBuilder::TABLE_MEMBERSHIP,
			$aliasMembership,
			$expr->andX(
				$this->exprLimit('single_id', $initiator->getSingleId(), $aliasMembership),
				$expr->eq($aliasMembership . '.circle_id', $helperAlias . '.' . $field)
			)
		);


		$listMembershipCircleAlias = [$aliasMembership];
		if ($this->getBool('initiatorDirectMember', $options, false)) {
			try {
				$aliasDirectInitiator = $this->generateAlias($alias, self::DIRECT_INITIATOR, $options);
				$listMembershipCircleAlias[] = $aliasDirectInitiator;
			} catch (RequestBuilderException $e) {
				// meaning that this path does not require DIRECT_INITIATOR; can be safely ignored
			}
		}

		try {
			$aliasMembershipCircle = $this->generateAlias($aliasMembership, self::CONFIG, $options);
			$orXMembershipCircle = $expr->orX();
			array_map(
				function (string $alias) use ($orXMembershipCircle, $aliasMembershipCircle) {
					$orXMembershipCircle->add(
						$this->expr()->eq(
							$alias . '.circle_id',
							$aliasMembershipCircle . '.unique_id'
						)
					);
				},
				$listMembershipCircleAlias
			);

			$this->leftJoin(
				$aliasMembership,
				CoreRequestBuilder::TABLE_CIRCLE,
				$aliasMembershipCircle,
				$orXMembershipCircle
			);
		} catch (RequestBuilderException $e) {
			// meaning that this path (ie. self::$SQL_PATH) does not require CONFIG; can be safely ignored
		}

		if (!$this->getBool('getData', $options, false)) {
			return;
		}

		// bypass memberships
		if ($this->getBool('initiatorDirectMember', $options, false)) {
			try {
				$aliasDirectInitiator = $this->generateAlias($alias, self::DIRECT_INITIATOR, $options);
				$this->generateMemberSelectAlias($aliasDirectInitiator)
					 ->leftJoin(
					 	$helperAlias,
					 	CoreRequestBuilder::TABLE_MEMBER,
					 	$aliasDirectInitiator,
					 	$expr->andX(
					 		$this->exprLimit('single_id', $initiator->getSingleId(), $aliasDirectInitiator),
					 		$expr->eq($aliasDirectInitiator . '.circle_id', $helperAlias . '.' . $field)
					 	)
					 );
			} catch (RequestBuilderException $e) {
				// meaning that this path does not require DIRECT_INITIATOR; can be safely ignored
			}
		}


		try {
			$aliasInitiator = $this->generateAlias($alias, self::INITIATOR, $options);
			$this->leftJoin(
				$aliasMembership, CoreRequestBuilder::TABLE_MEMBER, $aliasInitiator,
				$expr->andX(
					$expr->eq($aliasMembership . '.inheritance_first', $aliasInitiator . '.single_id'),
					$expr->eq($aliasMembership . '.circle_id', $aliasInitiator . '.circle_id')
				)
			);

			$aliasInheritedBy = $this->generateAlias($aliasInitiator, self::INHERITED_BY);
			$this->leftJoin(
				$aliasInitiator, CoreRequestBuilder::TABLE_MEMBER, $aliasInheritedBy,
				$expr->andX(
					$expr->eq($aliasMembership . '.single_id', $aliasInheritedBy . '.single_id'),
					$expr->eq($aliasMembership . '.inheritance_last', $aliasInheritedBy . '.circle_id')
				)
			);

			$default = [];
			if ($this->getBool('emulateVisitor', $options)) {
				$default = [
					'user_id' => $initiator->getUserId(),
					'single_id' => $initiator->getSingleId(),
					'user_type' => $initiator->getUserType(),
					'cached_name' => $initiator->getDisplayName(),
					'instance' => $initiator->getInstance()
				];
			}
			$aliasInheritedByMembership = $this->generateAlias($aliasInheritedBy, self::MEMBERSHIPS);
			$this->generateMemberSelectAlias($aliasInitiator, $default)
				 ->generateMemberSelectAlias($aliasInheritedBy)
				 ->generateMembershipSelectAlias($aliasMembership, $aliasInheritedByMembership);
		} catch (RequestBuilderException $e) {
		}
	}


	public function completeProbeWithInitiator(
		string $alias,
		string $field = 'single_id',
		string $helperAlias = ''
	): void {
		if ($this->getType() !== QueryBuilder::SELECT) {
			return;
		}

		try {
			$aliasInitiator = $this->generateAlias($alias, self::INITIATOR);
		} catch (RequestBuilderException $e) {
			return;
		}

		$helperAlias = ($helperAlias === '') ? $alias : $helperAlias;

		$expr = $this->expr();
		$this->generateMemberSelectAlias($aliasInitiator)
			 ->leftJoin(
			 	$alias, CoreRequestBuilder::TABLE_MEMBER, $aliasInitiator,
			 	$expr->andX(
			 		$expr->eq($aliasInitiator . '.circle_id', $alias . '.unique_id'),
			 		$expr->eq($aliasInitiator . '.' . $field, $helperAlias . '.inheritance_first'),
			 	)
			 );
		//
		//		$this->leftJoinBasedOn($aliasInitiator);
	}

	/**
	 * @param string $alias
	 *
	 * @return ICompositeExpression
	 * @throws RequestBuilderException
	 */
	protected function limitInitiatorVisibility(string $alias): ICompositeExpression {
		$aliasMembership = $this->generateAlias($alias, self::MEMBERSHIPS, $options);
		$aliasMembershipCircle = $this->generateAlias($aliasMembership, self::CONFIG, $options);
		$levelCheck = [$aliasMembership];

		$directMember = '';
		if ($this->getBool('initiatorDirectMember', $options, false)) {
			$directMember = $this->generateAlias($alias, self::DIRECT_INITIATOR, $options);
			array_push($levelCheck, $directMember);
		}

		$expr = $this->expr();

		// Visibility to non-member is
		// - 0 (default), if initiator is member
		// - 2 (Personal), if initiator is owner)
		// - 4 (Visible to everyone)
		$orX = $expr->orX();

		// filterPersonalCircles will remove access to Personal Circles as Owner
		if (!$this->getBool('filterPersonalCircles', $options, false)) {
			$orX->add(
				$expr->andX(
					$this->exprLimitBitwise('config', Circle::CFG_PERSONAL, $aliasMembershipCircle),
					$expr->eq($aliasMembership . '.level', $this->createNamedParameter(Member::LEVEL_OWNER))
				)
			);
		}

		$minimumLevel = $this->getInt('minimumLevel', $options);
		$andXMember = $expr->andX();
		$orXLevelCheck = $expr->orX();

		array_map(
			function (string $alias) use ($orXLevelCheck, $minimumLevel) {
				$orXLevelCheck->add(
					$this->expr()->gte(
						$alias . '.level',
						$this->createNamedParameter($minimumLevel, self::PARAM_INT)
					)
				);
			},
			$levelCheck
		);
		$andXMember->add($orXLevelCheck);

		if (!$this->getBool('includePersonalCircles', $options, false)) {
			$andXMember->add(
				$this->exprFilterBitwise(
					'config', Circle::CFG_PERSONAL,
					$aliasMembershipCircle
				)
			);
		}
		$orX->add($andXMember);

		if ($directMember !== '' && $this->getBool('allowRequestingMembership', $options, false)) {
			$orX->add($expr->orX(
				$this->exprLimit('status', Member::STATUS_REQUEST, $directMember),
				$this->exprLimit('status', Member::STATUS_INVITED, $directMember)
			));
		}

		if ($minimumLevel === 0 && $alias === self::CIRCLE) {
			$orX->add($this->exprLimitBitwise('config', Circle::CFG_VISIBLE, $alias));
		}

		if ($this->getBool('includeNonVisibleCircles', $options)) {
			$andXNonVisible = $expr->andX();
			$andXNonVisible->add($this->exprLimitBitwise('config', Circle::CFG_OPEN, $alias));
			$andXNonVisible->add($this->exprFilterBitwise('config', Circle::CFG_VISIBLE, $alias));
			$orX->add($andXNonVisible);
		}

		if ($this->getBool('visitingSingleCircles', $options, false)) {
			$orX->add($this->exprLimitBitwise('config', Circle::CFG_SINGLE, $alias));
		}

		// if Member can be Visitor, we only filter access to Personal Circles
		if ($this->getBool('viewableThroughKeyhole', $options, false)) {
			$andOpen = $expr->andX();
			$andOpen->add($this->exprLimitBitwise('config', Circle::CFG_OPEN, $alias));
			$andOpen->add($this->exprLimitBitwise('config', Circle::CFG_VISIBLE, $alias));
			if (!$this->configService->getAppValueBool(ConfigService::KEYHOLE_CFG_REQUEST)) {
				$andOpen->add($this->exprFilterBitwise('config', Circle::CFG_REQUEST, $alias));
			}
			$orX->add($andOpen);
		}

		$this->andWhere($orX);

		return $orX;
	}


	/**
	 * @param string $aliasCircle
	 * @param CircleProbe $probe
	 */
	public function filterCircles(string $aliasCircle, CircleProbe $probe): void {
		$filter = $probe->filtered();
		$include = $probe->included();
		$config = ($filter | $include) - $include;
		$this->filterBitwise('config', $config, $aliasCircle);
	}


	/**
	 * Limit visibility on Sensitive information when search for members.
	 *
	 * @param string $alias
	 *
	 * @return ICompositeExpression
	 */
	private function limitRemoteVisibility_Sensitive_Members(string $alias): ICompositeExpression {
		$expr = $this->expr();
		$andPassive = $expr->andX();
		$andPassive->add(
			$expr->eq($alias . '.type', $this->createNamedParameter(RemoteInstance::TYPE_PASSIVE))
		);

		$orMemberOrLevel = $expr->orX();
		$orMemberOrLevel->add(
			$expr->eq($this->getDefaultSelectAlias() . '.instance', $alias . '.instance')
		);
		// TODO: do we need this ? (display members from the local instance)
		$orMemberOrLevel->add(
			$expr->emptyString($this->getDefaultSelectAlias() . '.instance')
		);

		$orMemberOrLevel->add(
			$expr->eq(
				$this->getDefaultSelectAlias() . '.level',
				$this->createNamedParameter(Member::LEVEL_OWNER)
			)
		);
		$andPassive->add($orMemberOrLevel);

		return $andPassive;
	}


	/**
	 * Link to storage/filecache
	 *
	 * @param string $aliasShare
	 *
	 * @throws RequestBuilderException
	 */
	public function leftJoinFileCache(string $aliasShare) {
		$expr = $this->expr();

		$aliasFileCache = $this->generateAlias($aliasShare, self::FILE_CACHE);
		$aliasStorages = $this->generateAlias($aliasFileCache, self::STORAGES);

		$this->generateSelectAlias(
			CoreRequestBuilder::$outsideTables[CoreRequestBuilder::TABLE_FILE_CACHE],
			$aliasFileCache,
			$aliasFileCache,
			[]
		)
			 ->generateSelectAlias(
			 	CoreRequestBuilder::$outsideTables[CoreRequestBuilder::TABLE_STORAGES],
			 	$aliasStorages,
			 	$aliasStorages,
			 	[]
			 )
			 ->leftJoin(
			 	$aliasShare, CoreRequestBuilder::TABLE_FILE_CACHE, $aliasFileCache,
			 	$expr->eq($aliasShare . '.file_source', $aliasFileCache . '.fileid')
			 )
			 ->leftJoin(
			 	$aliasFileCache, CoreRequestBuilder::TABLE_STORAGES, $aliasStorages,
			 	$expr->eq($aliasFileCache . '.storage', $aliasStorages . '.numeric_id')
			 );
	}


	/**
	 * @param string $aliasShare
	 * @param string $aliasShareMemberships
	 *
	 * @throws RequestBuilderException
	 */
	public function leftJoinShareChild(string $aliasShare, string $aliasShareMemberships = '') {
		$expr = $this->expr();

		$aliasShareChild = $this->generateAlias($aliasShare, self::SHARE);
		if ($aliasShareMemberships === '') {
			$aliasShareMemberships = $this->generateAlias($aliasShare, self::MEMBERSHIPS, $options);
		}

		$this->leftJoin(
			$aliasShareMemberships, CoreRequestBuilder::TABLE_SHARE, $aliasShareChild,
			$expr->andX(
				$expr->eq($aliasShareChild . '.parent', $aliasShare . '.id'),
				$expr->eq($aliasShareChild . '.share_with', $aliasShareMemberships . '.single_id')
			)
		);

		$this->generateSelectAlias(
			['id', 'file_target', 'permissions'],
			$aliasShareChild,
			'child',
			[]
		);

		//		$this->selectAlias($aliasShareParent . '.permissions', 'parent_perms');
	}


	/**
	 * @param string $alias
	 * @param FederatedUser $federatedUser
	 * @param bool $reshares
	 */
	public function limitToShareOwner(
		string $alias,
		FederatedUser $federatedUser,
		bool $reshares,
		int $nodeId = 0
	): void {
		$expr = $this->expr();

		if ($reshares === false) {
			$this->andWhere($this->exprLimit('uid_initiator', $federatedUser->getUserId(), $alias));
		} elseif ($nodeId === 0) {
			$this->andWhere(
				$expr->orX(
					$this->exprLimit('uid_owner', $federatedUser->getUserId(), $alias),
					$this->exprLimit('uid_initiator', $federatedUser->getUserId(), $alias)
				)
			);
		}
	}


	/**
	 * @param string $aliasMount
	 * @param string $aliasMountMemberships
	 *
	 * @throws RequestBuilderException
	 */
	public function leftJoinMountpoint(string $aliasMount, IFederatedUser $federatedUser, string $aliasMountMemberships = '') {
		$expr = $this->expr();

		$aliasMountpoint = $this->generateAlias($aliasMount, self::MOUNTPOINT);
		if ($aliasMountMemberships === '') {
			$aliasMountMemberships = $this->generateAlias($aliasMount, self::MEMBERSHIPS, $options);
		}

		$this->leftJoin(
			$aliasMountMemberships, CoreRequestBuilder::TABLE_MOUNTPOINT, $aliasMountpoint,
			$expr->andX(
				$expr->eq($aliasMountpoint . '.mount_id', $aliasMount . '.mount_id'),
				$expr->eq($aliasMountpoint . '.single_id', $this->createNamedParameter($federatedUser->getSingleId()))
			)
		);

		$this->selectAlias($aliasMountpoint . '.mountpoint', $aliasMountpoint . '_mountpoint');
		$this->selectAlias($aliasMountpoint . '.mountpoint_hash', $aliasMountpoint . '_mountpoint_hash');
	}


	/**
	 * @param string $alias
	 * @param array $default
	 *
	 * @return CoreQueryBuilder
	 */
	private function generateCircleSelectAlias(string $alias, array $default = []): self {
		$this->generateSelectAlias(
			CoreRequestBuilder::$tables[CoreRequestBuilder::TABLE_CIRCLE],
			$alias,
			$alias,
			$default
		);

		return $this;
	}

	/**
	 * @param string $alias
	 * @param array $default
	 *
	 * @return $this
	 */
	private function generateMemberSelectAlias(string $alias, array $default = []): self {
		$this->generateSelectAlias(
			CoreRequestBuilder::$tables[CoreRequestBuilder::TABLE_MEMBER],
			$alias,
			$alias,
			$default
		);

		return $this;
	}


	/**
	 * @param string $alias
	 * @param array $default
	 * @param string $prefix
	 *
	 * @return $this
	 */
	private function generateMembershipSelectAlias(
		string $alias,
		string $prefix = '',
		array $default = []
	): self {
		$this->generateSelectAlias(
			CoreRequestBuilder::$tables[CoreRequestBuilder::TABLE_MEMBERSHIP],
			$alias,
			($prefix === '') ? $alias : $prefix,
			$default
		);

		return $this;
	}


	/**
	 * @param string $alias
	 * @param array $default
	 *
	 * @return $this
	 */
	private function generateRemoteInstanceSelectAlias(string $alias, array $default = []): self {
		$this->generateSelectAlias(
			CoreRequestBuilder::$tables[CoreRequestBuilder::TABLE_REMOTE],
			$alias,
			$alias,
			$default
		);

		return $this;
	}


	/**
	 * @param array $path
	 * @param array $options
	 *
	 * @return CoreQueryBuilder
	 */
	public function setOptions(array $path, array $options): self {
		$options = [self::OPTIONS => $options];
		foreach (array_reverse($path) as $item) {
			$options = [$item => $options];
		}

		$this->options = array_merge_recursive($this->options, $options);

		return $this;
	}


	/**
	 * @param string $base
	 * @param string $extension
	 * @param array|null $options
	 *
	 * @return string
	 * @throws RequestBuilderException
	 */
	public function generateAlias(string $base, string $extension, ?array &$options = []): string {
		$search = str_replace('_', '.', $base);
		$path = $search . '.' . $extension;
		if (!$this->validKey($path, $this->getSqlPath())
			&& !in_array($extension, $this->getArray($search, $this->getSqlPath()))) {
			throw new RequestBuilderException($extension . ' not found in ' . $search);
		}

		if (!is_array($options)) {
			$options = [];
		}

		$optionPath = '';
		foreach (explode('.', $path) as $p) {
			$optionPath = trim($optionPath . '.' . $p, '.');
			$options = array_merge(
				$options,
				$this->getArray($optionPath . '.' . self::OPTIONS, $this->getSqlPath()),
				$this->getArray($optionPath . '.' . self::OPTIONS, $this->options)
			);
		}

		return $base . '_' . $extension;
	}


	/**
	 * @param string $prefix
	 *
	 * @return array
	 */
	public function getAvailablePath(string $prefix): array {
		$prefix = trim($prefix, '_');
		$search = str_replace('_', '.', $prefix);

		$path = [];
		foreach ($this->getArray($search, $this->getSqlPath()) as $arr => $item) {
			if (is_numeric($arr)) {
				$k = $item;
			} else {
				$k = $arr;
			}
			$path[$k] = $prefix . '_' . $k . '_';
		}

		return $path;
	}


	/**
	 * @return array
	 */
	public function getSqlPath(): array {
		if (empty($this->sqlPath)) {
			return self::$SQL_PATH;
		}

		return $this->sqlPath;
	}


	/**
	 * DataProbe uses this to set which data need to be extracted, based on self::$SQL_PATH.
	 *
	 * @param string $key
	 * @param array $path
	 *
	 * @return $this
	 */
	public function setSqlPath(string $key, array $path = []): self {
		if (empty($this->sqlPath)) {
			$this->sqlPath = self::$SQL_PATH;
		}

		$this->sqlPath[$key] = $path;

		return $this;
	}

	public function resetSqlPath(): self {
		$this->sqlPath = [];

		return $this;
	}
}
