<?php declare(strict_types = 1);

namespace SlevomatCodingStandard\Helpers;

use PHP_CodeSniffer\Files\File;
use function array_filter;
use function array_reverse;
use function array_slice;
use function array_values;
use function count;
use function defined;
use function explode;
use function implode;
use function ltrim;
use function min;
use function sprintf;
use function strcasecmp;
use function strcmp;
use function strpos;
use const T_NAME_FULLY_QUALIFIED;
use const T_NAMESPACE;
use const T_NS_SEPARATOR;

/**
 * Terms "unqualified", "qualified" and "fully qualified" have the same meaning as described here:
 * http://php.net/manual/en/language.namespaces.rules.php
 *
 * "Canonical" is a fully qualified name without the leading backslash.
 *
 * @internal
 */
class NamespaceHelper
{

	public const NAMESPACE_SEPARATOR = '\\';

	/**
	 * @return list<int>
	 */
	public static function getAllNamespacesPointers(File $phpcsFile): array
	{
		$tokens = $phpcsFile->getTokens();
		$lazyValue = static function () use ($phpcsFile, $tokens): array {
			$all = TokenHelper::findNextAll($phpcsFile, T_NAMESPACE, 0);
			$all = array_filter(
				$all,
				static function ($pointer) use ($phpcsFile, $tokens) {
					$next = TokenHelper::findNextEffective($phpcsFile, $pointer + 1);
					return $next === null || $tokens[$next]['code'] !== T_NS_SEPARATOR;
				},
			);

			return array_values($all);
		};

		return SniffLocalCache::getAndSetIfNotCached($phpcsFile, 'namespacePointers', $lazyValue);
	}

	public static function isFullyQualifiedName(string $typeName): bool
	{
		return StringHelper::startsWith($typeName, self::NAMESPACE_SEPARATOR);
	}

	public static function isFullyQualifiedPointer(File $phpcsFile, int $pointer): bool
	{
		return $phpcsFile->getTokens()[$pointer]['code'] === T_NAME_FULLY_QUALIFIED;
	}

	public static function getFullyQualifiedTypeName(string $typeName): string
	{
		if (self::isFullyQualifiedName($typeName)) {
			return $typeName;
		}

		return sprintf('%s%s', self::NAMESPACE_SEPARATOR, $typeName);
	}

	public static function hasNamespace(string $typeName): bool
	{
		$parts = self::getNameParts($typeName);

		return count($parts) > 1;
	}

	/**
	 * @return list<string>
	 */
	public static function getNameParts(string $name): array
	{
		$name = self::normalizeToCanonicalName($name);

		return explode(self::NAMESPACE_SEPARATOR, $name);
	}

	public static function getLastNamePart(string $name): string
	{
		return array_slice(self::getNameParts($name), -1)[0];
	}

	public static function getName(File $phpcsFile, int $namespacePointer): string
	{
		/** @var int $namespaceNameStartPointer */
		$namespaceNameStartPointer = TokenHelper::findNextEffective($phpcsFile, $namespacePointer + 1);
		$namespaceNameEndPointer = TokenHelper::findNextExcluding(
			$phpcsFile,
			TokenHelper::NAME_TOKEN_CODES,
			$namespaceNameStartPointer + 1,
		) - 1;

		return TokenHelper::getContent($phpcsFile, $namespaceNameStartPointer, $namespaceNameEndPointer);
	}

	public static function findCurrentNamespacePointer(File $phpcsFile, int $pointer): ?int
	{
		$allNamespacesPointers = array_reverse(self::getAllNamespacesPointers($phpcsFile));
		foreach ($allNamespacesPointers as $namespacesPointer) {
			if ($namespacesPointer < $pointer) {
				return $namespacesPointer;
			}
		}

		return null;
	}

	public static function findCurrentNamespaceName(File $phpcsFile, int $anyPointer): ?string
	{
		$namespacePointer = self::findCurrentNamespacePointer($phpcsFile, $anyPointer);
		if ($namespacePointer === null) {
			return null;
		}

		return self::getName($phpcsFile, $namespacePointer);
	}

	public static function getUnqualifiedNameFromFullyQualifiedName(string $name): string
	{
		$parts = self::getNameParts($name);
		return $parts[count($parts) - 1];
	}

	public static function isQualifiedName(string $name): bool
	{
		return strpos($name, self::NAMESPACE_SEPARATOR) !== false;
	}

	public static function normalizeToCanonicalName(string $fullyQualifiedName): string
	{
		return ltrim($fullyQualifiedName, self::NAMESPACE_SEPARATOR);
	}

	public static function isTypeInNamespace(string $typeName, string $namespace): bool
	{
		return StringHelper::startsWith(
			self::normalizeToCanonicalName($typeName) . '\\',
			$namespace . '\\',
		);
	}

	public static function resolveClassName(File $phpcsFile, string $nameAsReferencedInFile, int $currentPointer): string
	{
		return self::resolveName($phpcsFile, $nameAsReferencedInFile, ReferencedName::TYPE_CLASS, $currentPointer);
	}

	public static function resolveName(File $phpcsFile, string $nameAsReferencedInFile, string $type, int $currentPointer): string
	{
		if (self::isFullyQualifiedName($nameAsReferencedInFile)) {
			return $nameAsReferencedInFile;
		}

		$useStatements = UseStatementHelper::getUseStatementsForPointer($phpcsFile, $currentPointer);

		$uniqueId = UseStatement::getUniqueId($type, self::normalizeToCanonicalName($nameAsReferencedInFile));

		if (isset($useStatements[$uniqueId])) {
			return sprintf('%s%s', self::NAMESPACE_SEPARATOR, $useStatements[$uniqueId]->getFullyQualifiedTypeName());
		}

		$nameParts = self::getNameParts($nameAsReferencedInFile);
		$firstPartUniqueId = UseStatement::getUniqueId($type, $nameParts[0]);
		if (count($nameParts) > 1 && isset($useStatements[$firstPartUniqueId])) {
			return sprintf(
				'%s%s%s%s',
				self::NAMESPACE_SEPARATOR,
				$useStatements[$firstPartUniqueId]->getFullyQualifiedTypeName(),
				self::NAMESPACE_SEPARATOR,
				implode(self::NAMESPACE_SEPARATOR, array_slice($nameParts, 1)),
			);
		}

		$name = sprintf('%s%s', self::NAMESPACE_SEPARATOR, $nameAsReferencedInFile);

		if ($type === ReferencedName::TYPE_CONSTANT && defined($name)) {
			return $name;
		}

		$namespaceName = self::findCurrentNamespaceName($phpcsFile, $currentPointer);
		if ($namespaceName !== null) {
			$name = sprintf('%s%s%s', self::NAMESPACE_SEPARATOR, $namespaceName, $name);
		}
		return $name;
	}

	public static function compareStatements(string $fqnA, string $fqnB, bool $caseSensitive): int
	{
		$aNameParts = explode(self::NAMESPACE_SEPARATOR, ltrim($fqnA, self::NAMESPACE_SEPARATOR));
		$bNameParts = explode(self::NAMESPACE_SEPARATOR, ltrim($fqnB, self::NAMESPACE_SEPARATOR));

		$minPartsCount = min(count($aNameParts), count($bNameParts));
		for ($i = 0; $i < $minPartsCount; $i++) {
			$comparison = self::compare($aNameParts[$i], $bNameParts[$i], $caseSensitive);
			if ($comparison === 0) {
				continue;
			}

			return $comparison;
		}

		return count($aNameParts) <=> count($bNameParts);
	}

	private static function compare(string $a, string $b, bool $caseSensitive): int
	{
		if ($caseSensitive) {
			return strcmp($a, $b);
		}

		return strcasecmp($a, $b);
	}

}
