<?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\ClassNotation;

use PhpCsFixer\AbstractFixer;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\Fixer\ConfigurableFixerTrait;
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\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;

/**
 * @author Filippo Tessarotto <zoeslam@gmail.com>
 *
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
 *
 * @phpstan-type _AutogeneratedInputConfiguration array{
 *  private_methods?: bool,
 * }
 * @phpstan-type _AutogeneratedComputedConfiguration array{
 *  private_methods: bool,
 * }
 */
final class NoUnneededFinalMethodFixer extends AbstractFixer implements ConfigurableFixerInterface
{
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
    use ConfigurableFixerTrait;

    public function getDefinition(): FixerDefinitionInterface
    {
        return new FixerDefinition(
            'Removes `final` from methods where possible.',
            [
                new CodeSample(
                    '<?php
final class Foo
{
    final public function foo1() {}
    final protected function bar() {}
    final private function baz() {}
}

class Bar
{
    final private function bar1() {}
}
'
                ),
                new CodeSample(
                    '<?php
final class Foo
{
    final private function baz() {}
}

class Bar
{
    final private function bar1() {}
}
',
                    ['private_methods' => false]
                ),
            ],
            null,
            'Risky when child class overrides a `private` method.'
        );
    }

    public function isCandidate(Tokens $tokens): bool
    {
        if (!$tokens->isAllTokenKindsFound([T_FINAL, T_FUNCTION])) {
            return false;
        }

        if (\defined('T_ENUM') && $tokens->isTokenKindFound(T_ENUM)) { // @TODO: drop condition when PHP 8.1+ is required
            return true;
        }

        return $tokens->isTokenKindFound(T_CLASS);
    }

    public function isRisky(): bool
    {
        return true;
    }

    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
    {
        foreach ($this->getMethods($tokens) as $element) {
            $index = $element['method_final_index'];

            if ($element['method_of_enum'] || $element['class_is_final']) {
                $this->clearFinal($tokens, $index);

                continue;
            }

            if (!$element['method_is_private'] || false === $this->configuration['private_methods'] || $element['method_is_constructor']) {
                continue;
            }

            $this->clearFinal($tokens, $index);
        }
    }

    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
    {
        return new FixerConfigurationResolver([
            (new FixerOptionBuilder('private_methods', 'Private methods of non-`final` classes must not be declared `final`.'))
                ->setAllowedTypes(['bool'])
                ->setDefault(true)
                ->getOption(),
        ]);
    }

    /**
     * @return \Generator<array{
     *     classIndex: int,
     *     token: Token,
     *     type: string,
     *     class_is_final?: bool,
     *     method_final_index: int|null,
     *     method_is_constructor?: bool,
     *     method_is_private: bool,
     *     method_of_enum: bool
     * }>
     */
    private function getMethods(Tokens $tokens): \Generator
    {
        $tokensAnalyzer = new TokensAnalyzer($tokens);
        $modifierKinds = [T_PUBLIC, T_PROTECTED, T_PRIVATE, T_FINAL, T_ABSTRACT, T_STATIC];

        $enums = [];
        $classesAreFinal = [];

        foreach ($tokensAnalyzer->getClassyElements() as $index => $element) {
            if ('method' !== $element['type']) {
                continue; // not a method
            }

            $classIndex = $element['classIndex'];

            if (!\array_key_exists($classIndex, $enums)) {
                $enums[$classIndex] = \defined('T_ENUM') && $tokens[$classIndex]->isGivenKind(T_ENUM); // @TODO: drop condition when PHP 8.1+ is required
            }

            $element['method_final_index'] = null;
            $element['method_is_private'] = false;

            $previous = $index;

            do {
                $previous = $tokens->getPrevMeaningfulToken($previous);

                if ($tokens[$previous]->isGivenKind(T_PRIVATE)) {
                    $element['method_is_private'] = true;
                } elseif ($tokens[$previous]->isGivenKind(T_FINAL)) {
                    $element['method_final_index'] = $previous;
                }
            } while ($tokens[$previous]->isGivenKind($modifierKinds));

            if ($enums[$classIndex]) {
                $element['method_of_enum'] = true;

                yield $element;

                continue;
            }

            if (!\array_key_exists($classIndex, $classesAreFinal)) {
                $modifiers = $tokensAnalyzer->getClassyModifiers($classIndex);
                $classesAreFinal[$classIndex] = isset($modifiers['final']);
            }

            $element['method_of_enum'] = false;
            $element['class_is_final'] = $classesAreFinal[$classIndex];
            $element['method_is_constructor'] = '__construct' === strtolower($tokens[$tokens->getNextMeaningfulToken($index)]->getContent());

            yield $element;
        }
    }

    private function clearFinal(Tokens $tokens, ?int $index): void
    {
        if (null === $index) {
            return;
        }

        $tokens->clearAt($index);

        ++$index;

        if ($tokens[$index]->isWhitespace()) {
            $tokens->clearAt($index);
        }
    }
}
