<?php declare(strict_types = 1);

namespace SlevomatCodingStandard\Sniffs\Namespaces;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use SlevomatCodingStandard\Helpers\AnnotationHelper;
use SlevomatCodingStandard\Helpers\ClassHelper;
use SlevomatCodingStandard\Helpers\CommentHelper;
use SlevomatCodingStandard\Helpers\ConstantHelper;
use SlevomatCodingStandard\Helpers\DocCommentHelper;
use SlevomatCodingStandard\Helpers\FixerHelper;
use SlevomatCodingStandard\Helpers\FunctionHelper;
use SlevomatCodingStandard\Helpers\NamespaceHelper;
use SlevomatCodingStandard\Helpers\ReferencedName;
use SlevomatCodingStandard\Helpers\ReferencedNameHelper;
use SlevomatCodingStandard\Helpers\SniffSettingsHelper;
use SlevomatCodingStandard\Helpers\StringHelper;
use SlevomatCodingStandard\Helpers\TokenHelper;
use SlevomatCodingStandard\Helpers\TypeHelper;
use SlevomatCodingStandard\Helpers\TypeHintHelper;
use SlevomatCodingStandard\Helpers\UseStatement;
use SlevomatCodingStandard\Helpers\UseStatementHelper;
use stdClass;
use function array_filter;
use function array_flip;
use function array_key_exists;
use function array_map;
use function array_reduce;
use function array_values;
use function count;
use function defined;
use function function_exists;
use function in_array;
use function preg_quote;
use function preg_replace;
use function sprintf;
use function strpos;
use function strtolower;
use function substr;
use const T_DECLARE;
use const T_DOC_COMMENT_OPEN_TAG;
use const T_NAMESPACE;
use const T_OPEN_CURLY_BRACKET;
use const T_OPEN_TAG;
use const T_SEMICOLON;
use const T_WHITESPACE;

class ReferenceUsedNamesOnlySniff implements Sniff
{

	public const CODE_REFERENCE_VIA_FULLY_QUALIFIED_NAME = 'ReferenceViaFullyQualifiedName';

	public const CODE_REFERENCE_VIA_FULLY_QUALIFIED_NAME_WITHOUT_NAMESPACE = 'ReferenceViaFullyQualifiedNameWithoutNamespace';

	public const CODE_REFERENCE_VIA_FALLBACK_GLOBAL_NAME = 'ReferenceViaFallbackGlobalName';

	public const CODE_PARTIAL_USE = 'PartialUse';

	private const SOURCE_CODE = 1;
	private const SOURCE_ANNOTATION = 2;
	private const SOURCE_ANNOTATION_CONSTANT_FETCH = 3;
	private const SOURCE_ATTRIBUTE = 4;

	public bool $searchAnnotations = false;

	public bool $allowFullyQualifiedExceptions = false;

	public bool $allowFullyQualifiedGlobalClasses = false;

	public bool $allowFullyQualifiedGlobalFunctions = false;

	public bool $allowFallbackGlobalFunctions = true;

	public bool $allowFullyQualifiedGlobalConstants = false;

	public bool $allowFallbackGlobalConstants = true;

	public bool $allowWhenNoNamespace = true;

	/** @var list<string> */
	public array $specialExceptionNames = [];

	/** @var list<string> */
	public array $ignoredNames = [];

	public bool $allowPartialUses = true;

	/**
	 * If empty, all namespaces are required to be used
	 *
	 * @var list<string>
	 */
	public array $namespacesRequiredToUse = [];

	public bool $allowFullyQualifiedNameForCollidingClasses = false;

	public bool $allowFullyQualifiedNameForCollidingFunctions = false;

	public bool $allowFullyQualifiedNameForCollidingConstants = false;

	/** @var list<string>|null */
	private ?array $normalizedSpecialExceptionNames = null;

	/** @var list<string>|null */
	private ?array $normalizedIgnoredNames = null;

	/** @var list<string>|null */
	private ?array $normalizedNamespacesRequiredToUse = null;

	/**
	 * @return array<int, (int|string)>
	 */
	public function register(): array
	{
		return [
			T_OPEN_TAG,
		];
	}

	/**
	 * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
	 * @param int $openTagPointer
	 */
	public function process(File $phpcsFile, $openTagPointer): void
	{
		if (TokenHelper::findPrevious($phpcsFile, T_OPEN_TAG, $openTagPointer - 1) !== null) {
			return;
		}

		$namespacePointers = NamespaceHelper::getAllNamespacesPointers($phpcsFile);

		if ($namespacePointers === [] && !$this->allowWhenNoNamespace) {
			return;
		}

		$tokens = $phpcsFile->getTokens();

		$references = $this->getReferences($phpcsFile, $openTagPointer);

		$definedClassesIndex = [];
		foreach (ClassHelper::getAllNames($phpcsFile) as $definedClassPointer => $definedClassName) {
			$definedClassesIndex[strtolower($definedClassName)] = NamespaceHelper::resolveClassName(
				$phpcsFile,
				$definedClassName,
				$definedClassPointer,
			);
		}
		$definedFunctionsIndex = array_flip(
			array_map(
				static fn (string $functionName): string => strtolower($functionName),
				FunctionHelper::getAllFunctionNames($phpcsFile),
			),
		);
		$definedConstantsIndex = array_flip(ConstantHelper::getAllNames($phpcsFile));

		$classReferencesIndex = [];
		$classReferences = array_filter(
			$references,
			static fn (stdClass $reference): bool => $reference->source === self::SOURCE_CODE && $reference->isClass,
		);

		foreach ($classReferences as $classReference) {
			$classReferencesIndex[strtolower($classReference->name)] = NamespaceHelper::resolveName(
				$phpcsFile,
				$classReference->name,
				$classReference->type,
				$classReference->startPointer,
			);
		}

		$referenceErrors = [];

		foreach ($references as $reference) {
			$useStatements = UseStatementHelper::getUseStatementsForPointer($phpcsFile, $reference->startPointer);

			$name = $reference->name;
			/** @var int $startPointer */
			$startPointer = $reference->startPointer;
			$canonicalName = NamespaceHelper::normalizeToCanonicalName($name);
			$unqualifiedName = NamespaceHelper::getUnqualifiedNameFromFullyQualifiedName($name);

			if (in_array(strtolower($unqualifiedName), ['true', 'false', 'null'], true)) {
				continue;
			}

			$collidingUseStatementUniqueId = UseStatement::getUniqueId($reference->type, $unqualifiedName);

			$isPartialUse = false;
			foreach ($useStatements as $useStatement) {
				$useStatementName = $useStatement->getAlias() ?? $useStatement->getNameAsReferencedInFile();
				if (strpos($name, $useStatementName . '\\') === 0) {
					$isPartialUse = true;
					break;
				}
			}

			$isFullyQualified = NamespaceHelper::isFullyQualifiedName($name)
				|| ($namespacePointers === [] && NamespaceHelper::hasNamespace($name) && !$isPartialUse);

			$isGlobalFallback = !$isFullyQualified
				&& !NamespaceHelper::hasNamespace($name)
				&& $namespacePointers !== []
				&& !array_key_exists(UseStatement::getUniqueId($reference->type, $name), $useStatements);

			$isGlobalFunctionFallback = false;
			if ($reference->isFunction && $isGlobalFallback) {
				$isGlobalFunctionFallback = !array_key_exists(strtolower($reference->name), $definedFunctionsIndex) && function_exists(
					$reference->name,
				);
			}
			$isGlobalConstantFallback = false;
			if ($reference->isConstant && $isGlobalFallback) {
				$isGlobalConstantFallback = !array_key_exists($reference->name, $definedConstantsIndex) && defined($reference->name);
			}

			if ($isFullyQualified) {
				if ($reference->isClass && $this->allowFullyQualifiedNameForCollidingClasses) {
					$lowerCasedUnqualifiedClassName = strtolower($unqualifiedName);
					if (
						array_key_exists($lowerCasedUnqualifiedClassName, $definedClassesIndex)
						&& $canonicalName !== NamespaceHelper::normalizeToCanonicalName(
							$definedClassesIndex[$lowerCasedUnqualifiedClassName],
						)
					) {
						continue;
					}

					if (
						array_key_exists($lowerCasedUnqualifiedClassName, $classReferencesIndex)
						&& $name !== $classReferencesIndex[$lowerCasedUnqualifiedClassName]
					) {
						continue;
					}

					if (
						array_key_exists($collidingUseStatementUniqueId, $useStatements)
						&& $canonicalName !== NamespaceHelper::normalizeToCanonicalName(
							$useStatements[$collidingUseStatementUniqueId]->getFullyQualifiedTypeName(),
						)
					) {
						continue;
					}
				} elseif ($reference->isFunction && $this->allowFullyQualifiedNameForCollidingFunctions) {
					$lowerCasedUnqualifiedFunctionName = strtolower($unqualifiedName);
					if (array_key_exists($lowerCasedUnqualifiedFunctionName, $definedFunctionsIndex)) {
						continue;
					}

					if (
						array_key_exists($collidingUseStatementUniqueId, $useStatements)
						&& $canonicalName !== NamespaceHelper::normalizeToCanonicalName(
							$useStatements[$collidingUseStatementUniqueId]->getFullyQualifiedTypeName(),
						)
					) {
						continue;
					}
				} elseif ($reference->isConstant && $this->allowFullyQualifiedNameForCollidingConstants) {
					if (array_key_exists($unqualifiedName, $definedConstantsIndex)) {
						continue;
					}

					if (
						array_key_exists($collidingUseStatementUniqueId, $useStatements)
						&& $canonicalName !== NamespaceHelper::normalizeToCanonicalName(
							$useStatements[$collidingUseStatementUniqueId]->getFullyQualifiedTypeName(),
						)
					) {
						continue;
					}
				}
			}

			if ($isFullyQualified || $isGlobalFunctionFallback || $isGlobalConstantFallback) {
				if ($isFullyQualified && !$this->isRequiredToBeUsed($name)) {
					continue;
				}

				$isExceptionByName = StringHelper::endsWith($name, 'Exception')
					|| $name === '\Throwable'
					|| (StringHelper::endsWith($name, 'Error') && !NamespaceHelper::hasNamespace($name))
					|| in_array($canonicalName, $this->getSpecialExceptionNames(), true);
				$inIgnoredNames = in_array($canonicalName, $this->getIgnoredNames(), true);

				if ($isExceptionByName && !$inIgnoredNames && $this->allowFullyQualifiedExceptions) {
					continue;
				}

				if (
					$isFullyQualified
					&& !NamespaceHelper::hasNamespace($name)
					&& $namespacePointers === []
				) {
					$label = sprintf(
						$reference->isConstant ? 'Constant %s' : ($reference->isFunction ? 'Function %s()' : 'Class %s'),
						$name,
					);

					$fix = $phpcsFile->addFixableError(sprintf(
						'%s should not be referenced via a fully qualified name, but via an unqualified name without the leading \\, because the file does not have a namespace and the type cannot be put in a use statement.',
						$label,
					), $startPointer, self::CODE_REFERENCE_VIA_FULLY_QUALIFIED_NAME_WITHOUT_NAMESPACE);
					if ($fix) {
						$phpcsFile->fixer->beginChangeset();

						if ($reference->source === self::SOURCE_ANNOTATION) {
							$fixedDocComment = AnnotationHelper::fixAnnotation(
								$reference->parsedDocComment,
								$reference->annotation,
								$reference->nameNode,
								new IdentifierTypeNode(substr($reference->name, 1)),
							);

							FixerHelper::change(
								$phpcsFile,
								$reference->parsedDocComment->getOpenPointer(),
								$reference->parsedDocComment->getClosePointer(),
								$fixedDocComment,
							);

						} elseif ($reference->source === self::SOURCE_ANNOTATION_CONSTANT_FETCH) {
							$fixedDocComment = AnnotationHelper::fixAnnotation(
								$reference->parsedDocComment,
								$reference->annotation,
								$reference->constantFetchNode,
								new ConstFetchNode(substr($reference->name, 1), $reference->constantFetchNode->name),
							);

							FixerHelper::change(
								$phpcsFile,
								$reference->parsedDocComment->getOpenPointer(),
								$reference->parsedDocComment->getClosePointer(),
								$fixedDocComment,
							);
						} else {
							FixerHelper::replace(
								$phpcsFile,
								$startPointer,
								substr($tokens[$startPointer]['content'], 1),
							);
						}

						$phpcsFile->fixer->endChangeset();
					}
				} else {
					$shouldBeUsed = NamespaceHelper::hasNamespace($name);
					if (!$shouldBeUsed) {
						if ($reference->isFunction) {
							$shouldBeUsed = $isFullyQualified
								? !$this->allowFullyQualifiedGlobalFunctions
								: !$this->allowFallbackGlobalFunctions;
						} elseif ($reference->isConstant) {
							$shouldBeUsed = $isFullyQualified
								? !$this->allowFullyQualifiedGlobalConstants
								: !$this->allowFallbackGlobalConstants;
						} else {
							$shouldBeUsed = !$this->allowFullyQualifiedGlobalClasses;
						}
					}

					if (!$shouldBeUsed) {
						continue;
					}

					$referenceErrors[] = (object) [
						'reference' => $reference,
						'canonicalName' => $canonicalName,
						'isGlobalConstantFallback' => $isGlobalConstantFallback,
						'isGlobalFunctionFallback' => $isGlobalFunctionFallback,
					];
				}
			} elseif (!$this->allowPartialUses) {
				if (NamespaceHelper::isQualifiedName($name)) {
					$phpcsFile->addError(sprintf(
						'Partial use statements are not allowed, but referencing %s found.',
						$name,
					), $startPointer, self::CODE_PARTIAL_USE);
				}
			}
		}

		if (count($referenceErrors) === 0) {
			return;
		}

		$alreadyAddedUses = [
			UseStatement::TYPE_CLASS => [],
			UseStatement::TYPE_FUNCTION => [],
			UseStatement::TYPE_CONSTANT => [],
		];

		$phpcsFile->fixer->beginChangeset();

		foreach ($referenceErrors as $referenceData) {
			$reference = $referenceData->reference;
			/** @var int $startPointer */
			$startPointer = $reference->startPointer;
			$canonicalName = $referenceData->canonicalName;
			$nameToReference = NamespaceHelper::getUnqualifiedNameFromFullyQualifiedName($reference->name);
			$canonicalNameToReference = $reference->isConstant ? $nameToReference : strtolower($nameToReference);
			$isGlobalConstantFallback = $referenceData->isGlobalConstantFallback;
			$isGlobalFunctionFallback = $referenceData->isGlobalFunctionFallback;

			$useStatements = UseStatementHelper::getUseStatementsForPointer($phpcsFile, $reference->startPointer);

			$canBeFixed = array_reduce(
				$alreadyAddedUses[$reference->type],
				static function (bool $carry, string $use) use ($canonicalName): bool {
					$useLastName = strtolower(NamespaceHelper::getLastNamePart($use));
					$canonicalLastName = strtolower(NamespaceHelper::getLastNamePart($canonicalName));
					return $useLastName === $canonicalLastName ? false : $carry;
				},
				true,
			);

			if (
				(
					$reference->isClass
					&& array_key_exists($canonicalNameToReference, $definedClassesIndex)
					&& $canonicalName !== NamespaceHelper::normalizeToCanonicalName($definedClassesIndex[$canonicalNameToReference])
				)
				|| (
					$reference->isClass
					&& array_key_exists($canonicalNameToReference, $classReferencesIndex)
					&& $canonicalName !== NamespaceHelper::normalizeToCanonicalName($classReferencesIndex[$canonicalNameToReference])
				)
				|| ($reference->isFunction && array_key_exists($canonicalNameToReference, $definedFunctionsIndex))
				|| ($reference->isConstant && array_key_exists($canonicalNameToReference, $definedConstantsIndex))
			) {
				$canBeFixed = false;
			}

			foreach ($useStatements as $useStatement) {
				if ($useStatement->getType() !== $reference->type) {
					continue;
				}

				if ($useStatement->getFullyQualifiedTypeName() === $canonicalName) {
					continue;
				}

				if ($useStatement->getCanonicalNameAsReferencedInFile() !== $canonicalNameToReference) {
					continue;
				}

				$canBeFixed = false;
				break;
			}

			$label = sprintf(
				$reference->isConstant
					? 'Constant %s'
					: ($reference->isFunction ? 'Function %s()' : 'Class %s'),
				$reference->name,
			);
			$errorCode = $isGlobalConstantFallback || $isGlobalFunctionFallback
				? self::CODE_REFERENCE_VIA_FALLBACK_GLOBAL_NAME
				: self::CODE_REFERENCE_VIA_FULLY_QUALIFIED_NAME;
			$errorMessage = $isGlobalConstantFallback || $isGlobalFunctionFallback
				? sprintf('%s should not be referenced via a fallback global name, but via a use statement.', $label)
				: sprintf('%s should not be referenced via a fully qualified name, but via a use statement.', $label);

			if (!$canBeFixed) {
				$phpcsFile->addError($errorMessage, $startPointer, $errorCode);
				continue;
			}

			$fix = $phpcsFile->addFixableError($errorMessage, $startPointer, $errorCode);

			if (!$fix) {
				continue;
			}

			$addUse = !in_array($canonicalName, $alreadyAddedUses[$reference->type], true);

			if (
				$reference->isClass
				&& array_key_exists($canonicalNameToReference, $definedClassesIndex)
			) {
				$addUse = false;
			}

			foreach ($useStatements as $useStatement) {
				if (
					$useStatement->getType() !== $reference->type
					|| $useStatement->getFullyQualifiedTypeName() !== $canonicalName
				) {
					continue;
				}

				$nameToReference = $useStatement->getNameAsReferencedInFile();
				$addUse = false;
				// Lock the use statement, so it is not modified by other sniffs
				FixerHelper::replace(
					$phpcsFile,
					$useStatement->getPointer(),
					$phpcsFile->fixer->getTokenContent($useStatement->getPointer()),
				);
				break;
			}

			if ($addUse) {
				$useStatementPlacePointer = $this->getUseStatementPlacePointer($phpcsFile, $openTagPointer, $useStatements);
				$useTypeName = UseStatement::getTypeName($reference->type);
				$useTypeFormatted = $useTypeName !== null ? sprintf('%s ', $useTypeName) : '';

				$phpcsFile->fixer->addNewline($useStatementPlacePointer);
				FixerHelper::add(
					$phpcsFile,
					$useStatementPlacePointer,
					sprintf('use %s%s;', $useTypeFormatted, $canonicalName),
				);

				$alreadyAddedUses[$reference->type][] = $canonicalName;
			}

			if ($reference->source === self::SOURCE_ANNOTATION) {
				$fixedDocComment = AnnotationHelper::fixAnnotation(
					$reference->parsedDocComment,
					$reference->annotation,
					$reference->nameNode,
					new IdentifierTypeNode($nameToReference),
				);

				FixerHelper::change(
					$phpcsFile,
					$reference->parsedDocComment->getOpenPointer(),
					$reference->parsedDocComment->getClosePointer(),
					$fixedDocComment,
				);

			} elseif ($reference->source === self::SOURCE_ANNOTATION_CONSTANT_FETCH) {
				$fixedDocComment = AnnotationHelper::fixAnnotation(
					$reference->parsedDocComment,
					$reference->annotation,
					$reference->constantFetchNode,
					new ConstFetchNode($nameToReference, $reference->constantFetchNode->name),
				);

				FixerHelper::change(
					$phpcsFile,
					$reference->parsedDocComment->getOpenPointer(),
					$reference->parsedDocComment->getClosePointer(),
					$fixedDocComment,
				);

			} elseif ($reference->source === self::SOURCE_ATTRIBUTE) {
				$attributeContent = TokenHelper::getContent($phpcsFile, $startPointer, $reference->endPointer);
				$fixedAttributeContent = preg_replace(
					'~(?<=\W)' . preg_quote($reference->name, '~') . '(?=\W)~',
					$nameToReference,
					$attributeContent,
				);
				FixerHelper::change($phpcsFile, $startPointer, $reference->endPointer, $fixedAttributeContent);
			} else {
				FixerHelper::change($phpcsFile, $startPointer, $reference->endPointer, $nameToReference);
			}
		}

		$phpcsFile->fixer->endChangeset();
	}

	/**
	 * @return list<string>
	 */
	private function getSpecialExceptionNames(): array
	{
		$this->normalizedSpecialExceptionNames ??= SniffSettingsHelper::normalizeArray($this->specialExceptionNames);

		return $this->normalizedSpecialExceptionNames;
	}

	/**
	 * @return list<string>
	 */
	private function getIgnoredNames(): array
	{
		$this->normalizedIgnoredNames ??= SniffSettingsHelper::normalizeArray($this->ignoredNames);

		return $this->normalizedIgnoredNames;
	}

	/**
	 * @return list<string>
	 */
	private function getNamespacesRequiredToUse(): array
	{
		$this->normalizedNamespacesRequiredToUse ??= SniffSettingsHelper::normalizeArray($this->namespacesRequiredToUse);

		return $this->normalizedNamespacesRequiredToUse;
	}

	/**
	 * @param array<string, UseStatement> $useStatements
	 */
	private function getUseStatementPlacePointer(File $phpcsFile, int $openTagPointer, array $useStatements): int
	{
		if (count($useStatements) !== 0) {
			$lastUseStatement = array_values($useStatements)[count($useStatements) - 1];
			/** @var int $useStatementPlacePointer */
			$useStatementPlacePointer = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $lastUseStatement->getPointer() + 1);
			return $useStatementPlacePointer;
		}

		$namespacePointer = TokenHelper::findNext($phpcsFile, T_NAMESPACE, $openTagPointer + 1);
		if ($namespacePointer !== null) {
			/** @var int $useStatementPlacePointer */
			$useStatementPlacePointer = TokenHelper::findNext($phpcsFile, [T_SEMICOLON, T_OPEN_CURLY_BRACKET], $namespacePointer + 1);
			return $useStatementPlacePointer;
		}

		$tokens = $phpcsFile->getTokens();

		$useStatementPlacePointer = $openTagPointer;
		if (
			substr($tokens[$openTagPointer]['content'], -1) !== $phpcsFile->eolChar
			&& $tokens[$openTagPointer + 1]['content'] === $phpcsFile->eolChar
		) {
			// @codeCoverageIgnoreStart
			$useStatementPlacePointer++;
			// @codeCoverageIgnoreEnd
		}

		$nonWhitespacePointerAfterOpenTag = TokenHelper::findNextNonWhitespace($phpcsFile, $openTagPointer + 1);
		if (in_array($tokens[$nonWhitespacePointerAfterOpenTag]['code'], Tokens::$commentTokens, true)) {
			$commentEndPointer = CommentHelper::getCommentEndPointer($phpcsFile, $nonWhitespacePointerAfterOpenTag);

			if (StringHelper::endsWith($tokens[$commentEndPointer]['content'], $phpcsFile->eolChar)) {
				$useStatementPlacePointer = $commentEndPointer;
			} else {
				$newLineAfterComment = $commentEndPointer + 1;

				if (array_key_exists($newLineAfterComment, $tokens) && $tokens[$newLineAfterComment]['content'] === $phpcsFile->eolChar) {
					$pointerAfterCommentEnd = TokenHelper::findNextNonWhitespace($phpcsFile, $newLineAfterComment + 1);

					if (TokenHelper::findNextContent(
						$phpcsFile,
						T_WHITESPACE,
						$phpcsFile->eolChar,
						$newLineAfterComment + 1,
						$pointerAfterCommentEnd,
					) !== null) {
						$useStatementPlacePointer = $commentEndPointer;
					}
				}
			}
		}

		$pointerAfter = TokenHelper::findNextEffective($phpcsFile, $useStatementPlacePointer + 1);
		if ($tokens[$pointerAfter]['code'] === T_DECLARE) {
			return TokenHelper::findNext($phpcsFile, T_SEMICOLON, $pointerAfter + 1);
		}

		return $useStatementPlacePointer;
	}

	private function isRequiredToBeUsed(string $name): bool
	{
		if ($this->namespacesRequiredToUse === []) {
			return true;
		}

		foreach ($this->getNamespacesRequiredToUse() as $namespace) {
			if (!NamespaceHelper::isTypeInNamespace($name, $namespace)) {
				continue;
			}

			return true;
		}

		return false;
	}

	/**
	 * @return list<stdClass>
	 */
	private function getReferences(File $phpcsFile, int $openTagPointer): array
	{
		$tokens = $phpcsFile->getTokens();

		$references = [];
		foreach (ReferencedNameHelper::getAllReferencedNames($phpcsFile, $openTagPointer) as $referencedName) {
			$reference = new stdClass();
			$reference->source = self::SOURCE_CODE;
			$reference->name = $referencedName->getNameAsReferencedInFile();
			$reference->type = $referencedName->getType();
			$reference->startPointer = $referencedName->getStartPointer();
			$reference->endPointer = $referencedName->getEndPointer();
			$reference->isClass = $referencedName->isClass();
			$reference->isConstant = $referencedName->isConstant();
			$reference->isFunction = $referencedName->isFunction();

			$references[] = $reference;
		}

		foreach (ReferencedNameHelper::getAllReferencedNamesInAttributes($phpcsFile, $openTagPointer) as $referencedName) {
			$reference = new stdClass();
			$reference->source = self::SOURCE_ATTRIBUTE;
			$reference->name = $referencedName->getNameAsReferencedInFile();
			$reference->type = $referencedName->getType();
			$reference->startPointer = $referencedName->getStartPointer();
			$reference->endPointer = $referencedName->getEndPointer();
			$reference->isClass = $referencedName->isClass();
			$reference->isConstant = $referencedName->isConstant();
			$reference->isFunction = $referencedName->isFunction();

			$references[] = $reference;
		}

		if (!$this->searchAnnotations) {
			return $references;
		}

		$searchAnnotationsPointer = $openTagPointer + 1;
		while (true) {
			$docCommentOpenPointer = TokenHelper::findNext($phpcsFile, T_DOC_COMMENT_OPEN_TAG, $searchAnnotationsPointer);
			if ($docCommentOpenPointer === null) {
				break;
			}

			$parsedDocComment = DocCommentHelper::parseDocComment($phpcsFile, $docCommentOpenPointer);

			if ($parsedDocComment !== null) {
				$annotations = AnnotationHelper::getAnnotations($phpcsFile, $docCommentOpenPointer);

				foreach ($annotations as $annotation) {
					$identifierTypeNodes = AnnotationHelper::getAnnotationNodesByType($annotation->getNode(), IdentifierTypeNode::class);

					foreach ($identifierTypeNodes as $typeHintNode) {
						$typeHint = $typeHintNode->name;

						$lowercasedTypeHint = strtolower($typeHint);
						if (
							TypeHintHelper::isSimpleTypeHint($lowercasedTypeHint)
							|| TypeHintHelper::isSimpleUnofficialTypeHints($lowercasedTypeHint)
							|| !TypeHelper::isTypeName($typeHint)
						) {
							continue;
						}

						$reference = new stdClass();
						$reference->source = self::SOURCE_ANNOTATION;
						$reference->parsedDocComment = $parsedDocComment;
						$reference->annotation = $annotation;
						$reference->nameNode = $typeHintNode;
						$reference->name = $typeHint;
						$reference->type = ReferencedName::TYPE_CLASS;
						$reference->startPointer = $annotation->getStartPointer();
						$reference->endPointer = null;
						$reference->isClass = true;
						$reference->isConstant = false;
						$reference->isFunction = false;

						$references[] = $reference;
					}

					$constantFetchNodes = AnnotationHelper::getAnnotationNodesByType($annotation->getNode(), ConstFetchNode::class);

					foreach ($constantFetchNodes as $constantFetchNode) {
						$reference = new stdClass();
						$reference->source = self::SOURCE_ANNOTATION_CONSTANT_FETCH;
						$reference->parsedDocComment = $parsedDocComment;
						$reference->annotation = $annotation;
						$reference->constantFetchNode = $constantFetchNode;
						$reference->name = $constantFetchNode->className;
						$reference->type = ReferencedName::TYPE_CLASS;
						$reference->startPointer = $annotation->getStartPointer();
						$reference->endPointer = null;
						$reference->isClass = true;
						$reference->isConstant = false;
						$reference->isFunction = false;

						$references[] = $reference;
					}
				}
			}

			$searchAnnotationsPointer = $tokens[$docCommentOpenPointer]['comment_closer'] + 1;
		}

		return $references;
	}

}
