<?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\WhitespacesAwareFixerInterface;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;

/**
 * @author Sander Verkuil <s.verkuil@pm.me>
 */
final class BlankLineBetweenImportGroupsFixer extends AbstractFixer implements WhitespacesAwareFixerInterface
{
    private const IMPORT_TYPE_CLASS = 'class';

    private const IMPORT_TYPE_CONST = 'const';

    private const IMPORT_TYPE_FUNCTION = 'function';

    /**
     * {@inheritdoc}
     */
    public function getDefinition(): FixerDefinitionInterface
    {
        return new FixerDefinition(
            'Putting blank lines between `use` statement groups.',
            [
                new CodeSample(
                    '<?php

use function AAC;
use const AAB;
use AAA;
'
                ),
                new CodeSample(
                    '<?php
use const AAAA;
use const BBB;
use Bar;
use AAC;
use Acme;
use function CCC\AA;
use function DDD;
'
                ),
                new CodeSample(
                    '<?php
use const BBB;
use const AAAA;
use Acme;
use AAC;
use Bar;
use function DDD;
use function CCC\AA;
'
                ),
                new CodeSample(
                    '<?php
use const AAAA;
use const BBB;
use Acme;
use function DDD;
use AAC;
use function CCC\AA;
use Bar;
'
                ),
            ]
        );
    }

    /**
     * {@inheritdoc}
     *
     * Must run after OrderedImportsFixer.
     */
    public function getPriority(): int
    {
        return -40;
    }

    /**
     * {@inheritdoc}
     */
    public function isCandidate(Tokens $tokens): bool
    {
        return $tokens->isTokenKindFound(T_USE);
    }

    /**
     * {@inheritdoc}
     */
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
    {
        $tokensAnalyzer = new TokensAnalyzer($tokens);
        $namespacesImports = $tokensAnalyzer->getImportUseIndexes(true);

        foreach (array_reverse($namespacesImports) as $uses) {
            $this->walkOverUses($tokens, $uses);
        }
    }

    /**
     * @param int[] $uses
     */
    private function walkOverUses(Tokens $tokens, array $uses): void
    {
        $usesCount = \count($uses);

        if ($usesCount < 2) {
            return; // nothing to fix
        }

        $previousType = null;

        for ($i = $usesCount - 1; $i >= 0; --$i) {
            $index = $uses[$i];
            $startIndex = $tokens->getNextMeaningfulToken($index + 1);
            $endIndex = $tokens->getNextTokenOfKind($startIndex, [';', [T_CLOSE_TAG]]);

            if ($tokens[$startIndex]->isGivenKind(CT::T_CONST_IMPORT)) {
                $type = self::IMPORT_TYPE_CONST;
            } elseif ($tokens[$startIndex]->isGivenKind(CT::T_FUNCTION_IMPORT)) {
                $type = self::IMPORT_TYPE_FUNCTION;
            } else {
                $type = self::IMPORT_TYPE_CLASS;
            }

            if (null !== $previousType && $type !== $previousType) {
                $this->ensureLine($tokens, $endIndex + 1);
            }

            $previousType = $type;
        }
    }

    private function ensureLine(Tokens $tokens, int $index): void
    {
        static $lineEnding;

        if (null === $lineEnding) {
            $lineEnding = $this->whitespacesConfig->getLineEnding();
            $lineEnding .= $lineEnding;
        }

        $index = $this->getInsertIndex($tokens, $index);

        if ($tokens[$index]->isWhitespace()) {
            $tokens[$index] = new Token([T_WHITESPACE, $lineEnding]);
        } else {
            $tokens->insertSlices([$index + 1 => [new Token([T_WHITESPACE, $lineEnding])]]);
        }
    }

    private function getInsertIndex(Tokens $tokens, int $index): int
    {
        $tokensCount = \count($tokens);

        for (; $index < $tokensCount - 1; ++$index) {
            if (!$tokens[$index]->isWhitespace() && !$tokens[$index]->isComment()) {
                return $index - 1;
            }

            $content = $tokens[$index]->getContent();

            if (str_contains($content, "\n")) {
                return $index;
            }
        }

        return $index;
    }
}
