<?php

declare (strict_types=1);
/*
 * This file is part of PHP CS Fixer.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */
namespace PhpCsFixer\Fixer\Whitespace;

use PhpCsFixer\AbstractFixer;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\Fixer\ConfigurableFixerTrait;
use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
use PhpCsFixer\FixerConfiguration\AllowedValueSubset;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Future;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Analyzer\SwitchAnalyzer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\FCT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;
/**
 * @phpstan-type _AutogeneratedInputConfiguration array{
 *  tokens?: list<'attribute'|'break'|'case'|'comma'|'continue'|'curly_brace_block'|'default'|'extra'|'parenthesis_brace_block'|'return'|'square_brace_block'|'switch'|'throw'|'use'|'use_trait'>,
 * }
 * @phpstan-type _AutogeneratedComputedConfiguration array{
 *  tokens: list<'attribute'|'break'|'case'|'comma'|'continue'|'curly_brace_block'|'default'|'extra'|'parenthesis_brace_block'|'return'|'square_brace_block'|'switch'|'throw'|'use'|'use_trait'>,
 * }
 *
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
 *
 * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
 *
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
 */
final class NoExtraBlankLinesFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface
{
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
    use ConfigurableFixerTrait;
    /**
     * @var non-empty-list<string>
     */
    private const AVAILABLE_TOKENS = ['attribute', 'break', 'case', 'comma', 'continue', 'curly_brace_block', 'default', 'extra', 'parenthesis_brace_block', 'return', 'square_brace_block', 'switch', 'throw', 'use', 'use_trait'];
    /**
     * @var array<int, callable(int): void> key is token id
     */
    private $tokenKindCallbackMap;
    /**
     * @var array<string, callable(int): void> key is token's content
     */
    private $tokenEqualsMap;
    /**
     * @var \PhpCsFixer\Tokenizer\Tokens
     */
    private $tokens;
    /**
     * @var \PhpCsFixer\Tokenizer\TokensAnalyzer
     */
    private $tokensAnalyzer;
    public function getDefinition() : FixerDefinitionInterface
    {
        return new FixerDefinition('Removes extra blank lines and/or blank lines following configuration.', [new CodeSample(<<<'PHP'
<?php

namespace ECSPrefix202510;

$foo = array("foo");
$bar = "bar";

PHP
), new CodeSample(<<<'PHP'
<?php

namespace ECSPrefix202510;

switch ($foo) {
    case 41:
        echo "foo";
        break;
    case 42:
        break;
}

PHP
, ['tokens' => ['break']]), new CodeSample(<<<'PHP'
<?php

namespace ECSPrefix202510;

for ($i = 0; $i < 9000; ++$i) {
    if (\true) {
        continue;
    }
}

PHP
, ['tokens' => ['continue']]), new CodeSample(<<<'PHP'
<?php

namespace ECSPrefix202510;

for ($i = 0; $i < 9000; ++$i) {
    echo $i;
}

PHP
, ['tokens' => ['curly_brace_block']]), new CodeSample(<<<'PHP'
<?php

namespace ECSPrefix202510;

$foo = array("foo");
$bar = "bar";

PHP
, ['tokens' => ['extra']]), new CodeSample(<<<'PHP'
<?php

namespace ECSPrefix202510;

$foo = array("foo");

PHP
, ['tokens' => ['parenthesis_brace_block']]), new CodeSample(<<<'PHP'
<?php

namespace ECSPrefix202510;

function foo($bar)
{
    return $bar;
}

PHP
, ['tokens' => ['return']]), new CodeSample(<<<'PHP'
<?php

namespace ECSPrefix202510;

$foo = ["foo"];

PHP
, ['tokens' => ['square_brace_block']]), new CodeSample(<<<'PHP'
<?php

namespace ECSPrefix202510;

function foo($bar)
{
    throw new \Exception("Hello!");
}

PHP
, ['tokens' => ['throw']]), new CodeSample(<<<'PHP'
<?php

namespace ECSPrefix202510\Foo;

use ECSPrefix202510\Bar\Baz;
use ECSPrefix202510\Baz\Bar;
class Bar
{
}

PHP
, ['tokens' => ['use']]), new CodeSample(<<<'PHP'
<?php

namespace ECSPrefix202510;

switch ($a) {
    case 1:
    default:
        echo 3;
}

PHP
, ['tokens' => ['switch', 'case', 'default']])]);
    }
    /**
     * {@inheritdoc}
     *
     * Must run before BlankLineBeforeStatementFixer.
     * Must run after ClassAttributesSeparationFixer, CombineConsecutiveUnsetsFixer, EmptyLoopBodyFixer, EmptyLoopConditionFixer, FunctionToConstantFixer, LongToShorthandOperatorFixer, ModernizeStrposFixer, NoEmptyCommentFixer, NoEmptyPhpdocFixer, NoEmptyStatementFixer, NoUnusedImportsFixer, NoUselessElseFixer, NoUselessPrintfFixer, NoUselessReturnFixer, NoUselessSprintfFixer, PhpdocReadonlyClassCommentToKeywordFixer, StringLengthToEmptyFixer, YieldFromArrayToYieldsFixer.
     */
    public function getPriority() : int
    {
        return -20;
    }
    public function isCandidate(Tokens $tokens) : bool
    {
        return \true;
    }
    /**
     * @param _AutogeneratedInputConfiguration $configuration
     */
    protected function configurePreNormalisation(array $configuration) : void
    {
        if (isset($configuration['tokens']) && \in_array('use_trait', $configuration['tokens'], \true)) {
            Future::triggerDeprecation(new \RuntimeException('Option "tokens: use_trait" used in `no_extra_blank_lines` rule is deprecated, use the rule `class_attributes_separation` with `elements: trait_import` instead.'));
        }
    }
    protected function configurePostNormalisation() : void
    {
        $tokensConfiguration = $this->configuration['tokens'];
        $this->tokenEqualsMap = [];
        if (\in_array('comma', $tokensConfiguration, \true)) {
            $this->tokenEqualsMap[','] = [$this, 'fixAfterToken'];
        }
        if (\in_array('curly_brace_block', $tokensConfiguration, \true)) {
            $this->tokenEqualsMap['{'] = [$this, 'fixStructureOpenCloseIfMultiLine'];
            // i.e. not: CT::T_ARRAY_INDEX_CURLY_BRACE_OPEN
        }
        if (\in_array('parenthesis_brace_block', $tokensConfiguration, \true)) {
            $this->tokenEqualsMap['('] = [$this, 'fixStructureOpenCloseIfMultiLine'];
            // i.e. not: CT::T_BRACE_CLASS_INSTANTIATION_OPEN
        }
        // Each item requires explicit array-like callable, otherwise PHPStan will complain about unused private methods.
        $configMap = ['attribute' => [CT::T_ATTRIBUTE_CLOSE, [$this, 'fixAfterToken']], 'break' => [\T_BREAK, [$this, 'fixAfterToken']], 'case' => [\T_CASE, [$this, 'fixAfterCaseToken']], 'continue' => [\T_CONTINUE, [$this, 'fixAfterToken']], 'default' => [\T_DEFAULT, [$this, 'fixAfterToken']], 'extra' => [\T_WHITESPACE, [$this, 'removeMultipleBlankLines']], 'return' => [\T_RETURN, [$this, 'fixAfterToken']], 'square_brace_block' => [CT::T_ARRAY_SQUARE_BRACE_OPEN, [$this, 'fixStructureOpenCloseIfMultiLine']], 'switch' => [\T_SWITCH, [$this, 'fixAfterToken']], 'throw' => [\T_THROW, [$this, 'fixAfterThrowToken']], 'use' => [\T_USE, [$this, 'removeBetweenUse']], 'use_trait' => [CT::T_USE_TRAIT, [$this, 'removeBetweenUse']]];
        $this->tokenKindCallbackMap = [];
        foreach ($tokensConfiguration as $config) {
            if (isset($configMap[$config])) {
                $this->tokenKindCallbackMap[$configMap[$config][0]] = $configMap[$config][1];
            }
        }
    }
    protected function applyFix(\SplFileInfo $file, Tokens $tokens) : void
    {
        $this->tokens = $tokens;
        $this->tokensAnalyzer = new TokensAnalyzer($this->tokens);
        for ($index = $tokens->getSize() - 1; $index > 0; --$index) {
            $this->fixByToken($tokens[$index], $index);
        }
    }
    protected function createConfigurationDefinition() : FixerConfigurationResolverInterface
    {
        return new FixerConfigurationResolver([(new FixerOptionBuilder('tokens', 'List of tokens to fix.'))->setAllowedTypes(['string[]'])->setAllowedValues([new AllowedValueSubset(self::AVAILABLE_TOKENS)])->setDefault(['extra'])->getOption()]);
    }
    /**
     * @uses fixAfterToken()
     * @uses fixAfterCaseToken()
     * @uses fixAfterThrowToken()
     * @uses fixStructureOpenCloseIfMultiLine()
     * @uses removeBetweenUse()
     * @uses removeMultipleBlankLines()
     */
    private function fixByToken(Token $token, int $index) : void
    {
        foreach ($this->tokenKindCallbackMap as $kind => $callback) {
            if (!$token->isGivenKind($kind)) {
                continue;
            }
            \call_user_func_array($this->tokenKindCallbackMap[$token->getId()], [$index]);
            return;
        }
        foreach ($this->tokenEqualsMap as $equals => $callback) {
            if (!$token->equals($equals)) {
                continue;
            }
            \call_user_func_array($this->tokenEqualsMap[$token->getContent()], [$index]);
            return;
        }
    }
    private function removeBetweenUse(int $index) : void
    {
        $next = $this->tokens->getNextTokenOfKind($index, [';', [\T_CLOSE_TAG]]);
        if (null === $next || $this->tokens[$next]->isGivenKind(\T_CLOSE_TAG)) {
            return;
        }
        $nextUseCandidate = $this->tokens->getNextMeaningfulToken($next);
        if (null === $nextUseCandidate || !$this->tokens[$nextUseCandidate]->isGivenKind($this->tokens[$index]->getId()) || !$this->containsLinebreak($index, $nextUseCandidate)) {
            return;
        }
        $this->removeEmptyLinesAfterLineWithTokenAt($next);
    }
    private function removeMultipleBlankLines(int $index) : void
    {
        $expected = $this->tokens[$index - 1]->isGivenKind(\T_OPEN_TAG) && Preg::match('/\\R$/', $this->tokens[$index - 1]->getContent()) ? 1 : 2;
        $parts = Preg::split('/(.*\\R)/', $this->tokens[$index]->getContent(), -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY);
        $count = \count($parts);
        if ($count > $expected) {
            $this->tokens[$index] = new Token([\T_WHITESPACE, \implode('', \array_slice($parts, 0, $expected)) . \rtrim($parts[$count - 1], "\r\n")]);
        }
    }
    private function fixAfterToken(int $index) : void
    {
        for ($i = $index - 1; $i > 0; --$i) {
            if ($this->tokens[$i]->isGivenKind(\T_FUNCTION) && $this->tokensAnalyzer->isLambda($i)) {
                return;
            }
            if ($this->tokens[$i]->isGivenKind(\T_CLASS) && $this->tokensAnalyzer->isAnonymousClass($i)) {
                return;
            }
            if ($this->tokens[$i]->isGivenKind([CT::T_ARRAY_SQUARE_BRACE_OPEN, CT::T_DESTRUCTURING_SQUARE_BRACE_OPEN])) {
                return;
            }
            if ($this->tokens[$i]->isWhitespace() && \strpos($this->tokens[$i]->getContent(), "\n") !== \false) {
                break;
            }
        }
        $this->removeEmptyLinesAfterLineWithTokenAt($index);
    }
    private function fixAfterCaseToken(int $index) : void
    {
        $enumSwitchIndex = $this->tokens->getPrevTokenOfKind($index, [[\T_SWITCH], [FCT::T_ENUM]]);
        if (!$this->tokens[$enumSwitchIndex]->isGivenKind(\T_SWITCH)) {
            return;
        }
        $this->removeEmptyLinesAfterLineWithTokenAt($index);
    }
    private function fixAfterThrowToken(int $index) : void
    {
        $prevIndex = $this->tokens->getPrevMeaningfulToken($index);
        if (!$this->tokens[$prevIndex]->equalsAny([';', '{', '}', ':', [\T_OPEN_TAG]])) {
            return;
        }
        if ($this->tokens[$prevIndex]->equals(':') && !SwitchAnalyzer::belongsToSwitch($this->tokens, $prevIndex)) {
            return;
        }
        $this->fixAfterToken($index);
    }
    /**
     * Remove white line(s) after the index of a block type,
     * but only if the block is not on one line.
     *
     * @param int $index body start
     */
    private function fixStructureOpenCloseIfMultiLine(int $index) : void
    {
        $blockTypeInfo = Tokens::detectBlockType($this->tokens[$index]);
        $bodyEnd = $this->tokens->findBlockEnd($blockTypeInfo['type'], $index);
        for ($i = $bodyEnd - 1; $i >= $index; --$i) {
            if (\strpos($this->tokens[$i]->getContent(), "\n") !== \false) {
                $this->removeEmptyLinesAfterLineWithTokenAt($i);
                $this->removeEmptyLinesAfterLineWithTokenAt($index);
                break;
            }
        }
    }
    private function removeEmptyLinesAfterLineWithTokenAt(int $index) : void
    {
        // find the line break
        $parenthesesDepth = 0;
        $tokenCount = \count($this->tokens);
        for ($end = $index; $end < $tokenCount; ++$end) {
            if ($this->tokens[$end]->equals('(')) {
                ++$parenthesesDepth;
                continue;
            }
            if ($this->tokens[$end]->equals(')')) {
                --$parenthesesDepth;
                if ($parenthesesDepth < 0) {
                    return;
                }
                continue;
            }
            if ($this->tokens[$end]->equals('}') || \strpos($this->tokens[$end]->getContent(), "\n") !== \false) {
                break;
            }
        }
        if ($end === $tokenCount) {
            return;
            // not found, early return
        }
        $ending = $this->whitespacesConfig->getLineEnding();
        for ($i = $end; $i < $tokenCount && $this->tokens[$i]->isWhitespace(); ++$i) {
            $content = $this->tokens[$i]->getContent();
            if (\substr_count($content, "\n") < 1) {
                continue;
            }
            $newContent = Preg::replace('/^.*\\R(\\h*)$/s', $ending . '$1', $content);
            $this->tokens[$i] = new Token([\T_WHITESPACE, $newContent]);
        }
    }
    private function containsLinebreak(int $startIndex, int $endIndex) : bool
    {
        for ($i = $endIndex; $i > $startIndex; --$i) {
            if (Preg::match('/\\R/', $this->tokens[$i]->getContent())) {
                return \true;
            }
        }
        return \false;
    }
}
