<?php declare(strict_types = 1);

namespace SlevomatCodingStandard\Sniffs\Arrays;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use SlevomatCodingStandard\Helpers\ScopeHelper;
use SlevomatCodingStandard\Helpers\TokenHelper;
use function array_key_exists;
use function array_reverse;
use function count;
use function in_array;
use const T_CLOSE_PARENTHESIS;
use const T_CLOSURE;
use const T_COMMA;
use const T_DOUBLE_COLON;
use const T_EQUAL;
use const T_FOREACH;
use const T_GLOBAL;
use const T_LIST;
use const T_OBJECT_OPERATOR;
use const T_OPEN_PARENTHESIS;
use const T_OPEN_SHORT_ARRAY;
use const T_OPEN_SQUARE_BRACKET;
use const T_OPEN_TAG;
use const T_STATIC;
use const T_STRING;
use const T_USE;
use const T_VARIABLE;

class DisallowImplicitArrayCreationSniff implements Sniff
{

	public const CODE_IMPLICIT_ARRAY_CREATION_USED = 'ImplicitArrayCreationUsed';

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

	/**
	 * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
	 * @param int $bracketOpenerPointer
	 */
	public function process(File $phpcsFile, $bracketOpenerPointer): void
	{
		$tokens = $phpcsFile->getTokens();

		$assignmentPointer = TokenHelper::findNextEffective($phpcsFile, $tokens[$bracketOpenerPointer]['bracket_closer'] + 1);
		if ($tokens[$assignmentPointer]['code'] !== T_EQUAL) {
			return;
		}

		/** @var int $variablePointer */
		$variablePointer = TokenHelper::findPreviousEffective($phpcsFile, $bracketOpenerPointer - 1);
		if ($tokens[$variablePointer]['code'] !== T_VARIABLE) {
			return;
		}

		if (in_array($tokens[$variablePointer]['content'], [
			'$GLOBALS',
			'$_SERVER',
			'$_REQUEST',
			'$_POST',
			'$_GET',
			'$_FILES',
			'$_ENV',
			'$_COOKIE',
			'$_SESSION',
			'$this',
		], true)) {
			return;
		}

		$pointerBeforeVariable = TokenHelper::findPreviousEffective($phpcsFile, $variablePointer - 1);
		if (in_array($tokens[$pointerBeforeVariable]['code'], [T_OBJECT_OPERATOR, T_DOUBLE_COLON], true)) {
			return;
		}

		$scopeOwnerPointer = null;
		foreach (array_reverse($tokens[$variablePointer]['conditions'], true) as $conditionPointer => $conditionTokenCode) {
			if (!in_array($conditionTokenCode, TokenHelper::FUNCTION_TOKEN_CODES, true)) {
				continue;
			}

			$scopeOwnerPointer = $conditionPointer;
			break;
		}

		$scopeOwnerPointer ??= TokenHelper::findPrevious($phpcsFile, T_OPEN_TAG, $variablePointer - 1);

		$scopeOpenerPointer = $tokens[$scopeOwnerPointer]['code'] === T_OPEN_TAG
			? $scopeOwnerPointer
			: $tokens[$scopeOwnerPointer]['scope_opener'];
		$scopeCloserPointer = $tokens[$scopeOwnerPointer]['code'] === T_OPEN_TAG
			? count($tokens) - 1
			: $tokens[$scopeOwnerPointer]['scope_closer'];

		if (in_array($tokens[$scopeOwnerPointer]['code'], TokenHelper::FUNCTION_TOKEN_CODES, true)) {
			if ($this->isParameter($phpcsFile, $scopeOwnerPointer, $variablePointer)) {
				return;
			}

			if (
				$tokens[$scopeOwnerPointer]['code'] === T_CLOSURE
				&& $this->isInheritedVariable($phpcsFile, $scopeOwnerPointer, $variablePointer)
			) {
				return;
			}
		}

		if ($this->hasExplicitCreation($phpcsFile, $scopeOpenerPointer, $scopeCloserPointer, $variablePointer)) {
			return;
		}

		$phpcsFile->addError('Implicit array creation is disallowed.', $variablePointer, self::CODE_IMPLICIT_ARRAY_CREATION_USED);
	}

	private function isParameter(File $phpcsFile, int $functionPointer, int $variablePointer): bool
	{
		$tokens = $phpcsFile->getTokens();
		$variableName = $tokens[$variablePointer]['content'];

		$parameterPointer = TokenHelper::findNextContent(
			$phpcsFile,
			T_VARIABLE,
			$variableName,
			$tokens[$functionPointer]['parenthesis_opener'] + 1,
			$tokens[$functionPointer]['parenthesis_closer'],
		);
		return $parameterPointer !== null;
	}

	private function isInheritedVariable(File $phpcsFile, int $closurePointer, int $variablePointer): bool
	{
		$tokens = $phpcsFile->getTokens();
		$variableName = $tokens[$variablePointer]['content'];

		$usePointer = TokenHelper::findNext(
			$phpcsFile,
			T_USE,
			$tokens[$closurePointer]['parenthesis_closer'] + 1,
			$tokens[$closurePointer]['scope_opener'],
		);
		if ($usePointer === null) {
			return false;
		}

		$parenthesisOpenerPointer = TokenHelper::findNextEffective($phpcsFile, $usePointer + 1);

		$inheritedVariablePointer = TokenHelper::findNextContent(
			$phpcsFile,
			T_VARIABLE,
			$variableName,
			$parenthesisOpenerPointer + 1,
			$tokens[$parenthesisOpenerPointer]['parenthesis_closer'],
		);
		return $inheritedVariablePointer !== null;
	}

	private function hasExplicitCreation(File $phpcsFile, int $scopeOpenerPointer, int $scopeCloserPointer, int $variablePointer): bool
	{
		$tokens = $phpcsFile->getTokens();

		$variableName = $tokens[$variablePointer]['content'];

		for ($i = $scopeOpenerPointer + 1; $i < $variablePointer; $i++) {
			if ($tokens[$i]['code'] !== T_VARIABLE) {
				continue;
			}

			if ($tokens[$i]['content'] !== $variableName) {
				continue;
			}

			if (!ScopeHelper::isInSameScope($phpcsFile, $variablePointer, $i)) {
				continue;
			}

			$assignmentPointer = TokenHelper::findNextEffective($phpcsFile, $i + 1);
			if ($tokens[$assignmentPointer]['code'] === T_EQUAL) {
				return true;
			}

			$staticPointer = TokenHelper::findPreviousEffective($phpcsFile, $i - 1);
			if ($tokens[$staticPointer]['code'] === T_STATIC) {
				return true;
			}

			if ($this->isCreatedInForeach($phpcsFile, $i, $scopeCloserPointer)) {
				return true;
			}

			if ($this->isCreatedInList($phpcsFile, $i, $scopeOpenerPointer)) {
				return true;
			}

			if ($this->isCreatedByReferencedParameterInFunctionCall($phpcsFile, $i, $scopeOpenerPointer)) {
				return true;
			}

			if ($this->isImportedUsingGlobalStatement($phpcsFile, $i)) {
				return true;
			}
		}

		return false;
	}

	private function isCreatedInList(File $phpcsFile, int $variablePointer, int $scopeOpenerPointer): bool
	{
		$tokens = $phpcsFile->getTokens();

		$parenthesisOpenerPointer = TokenHelper::findPrevious(
			$phpcsFile,
			[T_OPEN_PARENTHESIS, T_OPEN_SHORT_ARRAY, T_OPEN_SQUARE_BRACKET],
			$variablePointer - 1,
			$scopeOpenerPointer,
		);
		if ($parenthesisOpenerPointer === null) {
			return false;
		}

		if ($tokens[$parenthesisOpenerPointer]['code'] === T_OPEN_PARENTHESIS) {
			if ($tokens[$parenthesisOpenerPointer]['parenthesis_closer'] < $variablePointer) {
				return false;
			}

			$pointerBeforeParenthesisOpener = TokenHelper::findPreviousEffective($phpcsFile, $parenthesisOpenerPointer - 1);
			return $tokens[$pointerBeforeParenthesisOpener]['code'] === T_LIST;
		}

		return $tokens[$parenthesisOpenerPointer]['bracket_closer'] > $variablePointer;
	}

	private function isCreatedInForeach(File $phpcsFile, int $variablePointer, int $scopeCloserPointer): bool
	{
		$tokens = $phpcsFile->getTokens();

		$parenthesisCloserPointer = TokenHelper::findNext($phpcsFile, T_CLOSE_PARENTHESIS, $variablePointer + 1, $scopeCloserPointer);
		return $parenthesisCloserPointer !== null
			&& array_key_exists('parenthesis_owner', $tokens[$parenthesisCloserPointer])
			&& $tokens[$tokens[$parenthesisCloserPointer]['parenthesis_owner']]['code'] === T_FOREACH
			&& $tokens[$parenthesisCloserPointer]['parenthesis_opener'] < $variablePointer;
	}

	private function isCreatedByReferencedParameterInFunctionCall(File $phpcsFile, int $variablePointer, int $scopeOpenerPointer): bool
	{
		$tokens = $phpcsFile->getTokens();

		$parenthesisOpenerPointer = TokenHelper::findPrevious($phpcsFile, T_OPEN_PARENTHESIS, $variablePointer - 1, $scopeOpenerPointer);

		if (
			$parenthesisOpenerPointer === null
			|| $tokens[$parenthesisOpenerPointer]['parenthesis_closer'] < $variablePointer
		) {
			return false;
		}

		$pointerBeforeParenthesisOpener = TokenHelper::findPreviousEffective($phpcsFile, $parenthesisOpenerPointer - 1);
		return $tokens[$pointerBeforeParenthesisOpener]['code'] === T_STRING;
	}

	private function isImportedUsingGlobalStatement(File $phpcsFile, int $variablePointer): bool
	{
		$tokens = $phpcsFile->getTokens();

		$startOfStatement = $phpcsFile->findStartOfStatement($variablePointer, T_COMMA);

		return $tokens[$startOfStatement]['code'] === T_GLOBAL;
	}

}
