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

use PhpCsFixer\AbstractPhpdocTypesFixer;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\Fixer\ConfigurableFixerTrait;
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;

/**
 * @phpstan-type _AutogeneratedInputConfiguration array{
 *  types?: list<'boolean'|'callback'|'double'|'integer'|'never-return'|'never-returns'|'no-return'|'real'|'str'>,
 * }
 * @phpstan-type _AutogeneratedComputedConfiguration array{
 *  types: list<'boolean'|'callback'|'double'|'integer'|'never-return'|'never-returns'|'no-return'|'real'|'str'>,
 * }
 *
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
 *
 * @author Graham Campbell <hello@gjcampbell.co.uk>
 *
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
 */
final class PhpdocScalarFixer extends AbstractPhpdocTypesFixer implements ConfigurableFixerInterface
{
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
    use ConfigurableFixerTrait;

    /**
     * The types to fix.
     */
    private const TYPES_MAP = [
        'boolean' => 'bool',
        'callback' => 'callable',
        'double' => 'float',
        'integer' => 'int',
        'never-return' => 'never',
        'never-returns' => 'never',
        'no-return' => 'never',
        'real' => 'float',
        'str' => 'string',
    ];

    public function getDefinition(): FixerDefinitionInterface
    {
        return new FixerDefinition(
            'Scalar types should always be written in the same form. `int` not `integer`, `bool` not `boolean`, `float` not `real` or `double`.',
            [
                new CodeSample(
                    <<<'PHP'
                        <?php
                        /**
                         * @param integer $a
                         * @param boolean $b
                         * @param real $c
                         *
                         * @return double
                         */
                        function sample($a, $b, $c)
                        {
                            return sample2($a, $b, $c);
                        }

                        PHP,
                ),
                new CodeSample(
                    <<<'PHP'
                        <?php
                        /**
                         * @param integer $a
                         * @param boolean $b
                         * @param real $c
                         */
                        function sample($a, $b, $c)
                        {
                            return sample2($a, $b, $c);
                        }

                        PHP,
                    ['types' => ['boolean']],
                ),
            ],
        );
    }

    /**
     * {@inheritdoc}
     *
     * Must run before GeneralPhpdocAnnotationRemoveFixer, GeneralPhpdocTagRenameFixer, NoBlankLinesAfterPhpdocFixer, NoEmptyPhpdocFixer, NoSuperfluousPhpdocTagsFixer, PhpdocAddMissingParamAnnotationFixer, PhpdocAlignFixer, PhpdocArrayTypeFixer, PhpdocInlineTagNormalizerFixer, PhpdocLineSpanFixer, PhpdocListTypeFixer, PhpdocNoAccessFixer, PhpdocNoAliasTagFixer, PhpdocNoDuplicateTypesFixer, PhpdocNoEmptyReturnFixer, PhpdocNoPackageFixer, PhpdocNoUselessInheritdocFixer, PhpdocOrderByValueFixer, PhpdocOrderFixer, PhpdocParamOrderFixer, PhpdocReadonlyClassCommentToKeywordFixer, PhpdocReturnSelfReferenceFixer, PhpdocSeparationFixer, PhpdocSingleLineVarSpacingFixer, PhpdocSummaryFixer, PhpdocTagCasingFixer, PhpdocTagNoNamedArgumentsFixer, PhpdocTagTypeFixer, PhpdocToParamTypeFixer, PhpdocToPropertyTypeFixer, PhpdocToReturnTypeFixer, PhpdocTrimConsecutiveBlankLineSeparationFixer, PhpdocTrimFixer, PhpdocTypesNoDuplicatesFixer, PhpdocTypesOrderFixer, PhpdocVarAnnotationCorrectOrderFixer, PhpdocVarWithoutNameFixer.
     * Must run after PhpdocTypesFixer.
     */
    public function getPriority(): int
    {
        /*
         * Should be run before all other docblock fixers apart from the
         * phpdoc_to_comment and phpdoc_indent fixer to make sure all fixers
         * apply correct indentation to new code they add. This should run
         * before alignment of params is done since this fixer might change
         * the type and thereby un-aligning the params. We also must run after
         * the phpdoc_types_fixer because it can convert types to things that
         * we can fix.
         */
        return 15;
    }

    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
    {
        $defaultTypes = ['boolean', 'callback', 'double', 'integer', 'real', 'str'];
        $allowedTypes = array_keys(self::TYPES_MAP);

        return new FixerConfigurationResolver([
            (new FixerOptionBuilder('types', 'A list of types to fix.'))
                ->setAllowedValues([new AllowedValueSubset($allowedTypes)])
                ->setDefault(Future::getV4OrV3($allowedTypes, $defaultTypes))
                ->getOption(),
        ]);
    }

    protected function normalize(string $type): string
    {
        $suffix = '';
        while (str_ends_with($type, '[]')) {
            $type = substr($type, 0, -2);
            $suffix .= '[]';
        }

        if (\in_array($type, $this->configuration['types'], true)) {
            $type = self::TYPES_MAP[$type];
        }

        return $type.$suffix;
    }
}
