<?php
/**
 * PHPCompatibility, an external standard for PHP_CodeSniffer.
 *
 * @package   PHPCompatibility
 * @copyright 2012-2020 PHPCompatibility Contributors
 * @license   https://opensource.org/licenses/LGPL-3.0 LGPL3
 * @link      https://github.com/PHPCompatibility/PHPCompatibility
 */

namespace PHPCompatibility\Sniffs\FunctionUse;

use PHPCompatibility\AbstractFunctionCallParameterSniff;
use PHPCompatibility\Helpers\ScannedCode;
use PHP_CodeSniffer\Files\File;
use PHPCSUtils\Utils\MessageHelper;
use PHPCSUtils\Utils\PassedParameters;

/**
 * Detect missing required function parameters in calls to native PHP functions.
 *
 * Specifically when those function parameters are no longer required in more recent PHP versions.
 *
 * PHP version All
 *
 * @link https://www.php.net/manual/en/doc.changelog.php
 *
 * @since 7.0.3
 * @since 7.1.0  Now extends the `AbstractComplexVersionSniff` instead of the base `Sniff` class.
 * @since 9.0.0  Renamed from `RequiredOptionalFunctionParametersSniff` to `RequiredToOptionalFunctionParametersSniff`.
 * @since 10.0.0 - Now extends the base `AbstractFunctionCallParameterSniff` class.
 *                 Methods which were previously required due to the extending of the `AbstractComplexVersionSniff`
 *                 have been removed.
 *               - This class is now `final`.
 */
final class RequiredToOptionalFunctionParametersSniff extends AbstractFunctionCallParameterSniff
{

    /**
     * A list of function parameters, which were required in older versions and became optional later on.
     *
     * The array lists : version number with true (required) and false (optional).
     *
     * The index is the 1-based parameter position of the parameter in the parameter list.
     * If's sufficient to list the last version in which the parameter was still required.
     *
     * @since 7.0.3
     * @since 10.0.0 - Parameter renamed from `$functionParameters` to `$targetFunctions` for
     *                 compatibility with the `AbstractFunctionCallParameterSniff` class.
     *               - The parameter offsets were changed from 0-based to 1-based.
     *
     * @var array<string, array<int, array<string, bool|string>>>
     */
    protected $targetFunctions = [
        'array_diff_assoc' => [
            2 => [
                'name' => 'arrays',
                '7.4'  => true,
                '8.0'  => false,
            ],
        ],
        'array_diff_key' => [
            2 => [
                'name' => 'arrays',
                '7.4'  => true,
                '8.0'  => false,
            ],
        ],
        'array_diff_uassoc' => [
            /*
             * $rest is actually at position 2, but has another required parameter after it,
             * so we need to detect on the last parameter.
             */
            3 => [
                'name' => 'rest',
                '7.4'  => true,
                '8.0'  => false,
            ],
        ],
        'array_diff_ukey' => [
            // Note from array_diff_uassoc applies here too.
            3 => [
                'name' => 'rest',
                '7.4'  => true,
                '8.0'  => false,
            ],
        ],
        'array_diff' => [
            2 => [
                'name' => 'arrays',
                '7.4'  => true,
                '8.0'  => false,
            ],
        ],
        'array_intersect_assoc' => [
            2 => [
                'name' => 'arrays',
                '7.4'  => true,
                '8.0'  => false,
            ],
        ],
        'array_intersect_key' => [
            2 => [
                'name' => 'arrays',
                '7.4'  => true,
                '8.0'  => false,
            ],
        ],
        'array_intersect_uassoc' => [
            // Note from array_diff_uassoc applies here too.
            3 => [
                'name' => 'rest',
                '7.4'  => true,
                '8.0'  => false,
            ],
        ],
        'array_intersect_ukey' => [
            // Note from array_diff_uassoc applies here too.
            3 => [
                'name' => 'rest',
                '7.4'  => true,
                '8.0'  => false,
            ],
        ],
        'array_intersect' => [
            2 => [
                'name' => 'arrays',
                '7.4'  => true,
                '8.0'  => false,
            ],
        ],
        'array_merge' => [
            1 => [
                'name' => 'arrays',
                '7.3'  => true,
                '7.4'  => false,
            ],
        ],
        'array_merge_recursive' => [
            1 => [
                'name' => 'arrays',
                '7.3'  => true,
                '7.4'  => false,
            ],
        ],
        'array_push' => [
            2 => [
                'name' => 'values',
                '7.2'  => true,
                '7.3'  => false,
            ],
        ],
        'array_udiff_assoc' => [
            // Note from array_diff_uassoc applies here too.
            3 => [
                'name' => 'rest',
                '7.4'  => true,
                '8.0'  => false,
            ],
        ],
        'array_udiff_uassoc' => [
            // Note from array_diff_uassoc applies here too.
            4 => [
                'name' => 'rest',
                '7.4'  => true,
                '8.0'  => false,
            ],
        ],
        'array_udiff' => [
            // Note from array_diff_uassoc applies here too.
            3 => [
                'name' => 'rest',
                '7.4'  => true,
                '8.0'  => false,
            ],
        ],
        'array_uintersect_assoc' => [
            // Note from array_diff_uassoc applies here too.
            3 => [
                'name' => 'rest',
                '7.4'  => true,
                '8.0'  => false,
            ],
        ],
        'array_uintersect_uassoc' => [
            // Note from array_diff_uassoc applies here too.
            4 => [
                'name' => 'rest',
                '7.4'  => true,
                '8.0'  => false,
            ],
        ],
        'array_uintersect' => [
            // Note from array_diff_uassoc applies here too.
            3 => [
                'name' => 'rest',
                '7.4'  => true,
                '8.0'  => false,
            ],
        ],
        'array_unshift' => [
            2 => [
                'name' => 'values',
                '7.2'  => true,
                '7.3'  => false,
            ],
        ],
        'bcscale' => [
            1 => [
                'name' => 'scale',
                '7.2'  => true,
                '7.3'  => false,
            ],
        ],
        'fgetcsv' => [
            2 => [
                'name' => 'length',
                '5.0'  => true,
                '5.1'  => false,
            ],
        ],
        'ftp_fget' => [
            4 => [
                'name' => 'mode',
                '7.2'  => true,
                '7.3'  => false,
            ],
        ],
        'ftp_fput' => [
            4 => [
                'name' => 'mode',
                '7.2'  => true,
                '7.3'  => false,
            ],
        ],
        'ftp_get' => [
            4 => [
                'name' => 'mode',
                '7.2'  => true,
                '7.3'  => false,
            ],
        ],
        'ftp_nb_fget' => [
            4 => [
                'name' => 'mode',
                '7.2'  => true,
                '7.3'  => false,
            ],
        ],
        'ftp_nb_fput' => [
            4 => [
                'name' => 'mode',
                '7.2'  => true,
                '7.3'  => false,
            ],
        ],
        'ftp_nb_get' => [
            4 => [
                'name' => 'mode',
                '7.2'  => true,
                '7.3'  => false,
            ],
        ],
        'ftp_nb_put' => [
            4 => [
                'name' => 'mode',
                '7.2'  => true,
                '7.3'  => false,
            ],
        ],
        'ftp_put' => [
            4 => [
                'name' => 'mode',
                '7.2'  => true,
                '7.3'  => false,
            ],
        ],
        'getenv' => [
            1 => [
                'name' => 'name',
                '7.0'  => true,
                '7.1'  => false,
            ],
        ],
        'imagepolygon' => [
            /*
             * $points is actually at position 3, but has another required parameter after it,
             * so we need to detect on the last parameter.
             * Note: this function explicitly does NOT support named parameters for the function
             * signature without this parameter, but that's not the concern of this sniff.
             */
            4 => [
                'name' => 'num_points',
                '7.4'  => true,
                '8.0'  => false,
            ],
        ],
        'imageopenpolygon' => [
            // Note from imagepolygon applies here too.
            4 => [
                'name' => 'num_points',
                '7.4'  => true,
                '8.0'  => false,
            ],
        ],
        'imagefilledpolygon' => [
            // Note from imagepolygon applies here too.
            4 => [
                'name' => 'num_points',
                '7.4'  => true,
                '8.0'  => false,
            ],
        ],
        'pg_select' => [
            3 => [
                'name' => 'conditions',
                '8.3'  => true,
                '8.4'  => false,
            ],
        ],
        'preg_match_all' => [
            3 => [
                'name' => 'matches',
                '5.3'  => true,
                '5.4'  => false,
            ],
        ],
        'stream_socket_enable_crypto' => [
            3 => [
                'name' => 'crypto_method',
                '5.5'  => true,
                '5.6'  => false,
            ],
        ],
        'xmlwriter_write_element' => [
            3 => [
                'name'  => 'content',
                '5.2.2' => true,
                '5.2.3' => false,
            ],
        ],
        'xmlwriter_write_element_ns' => [
            5 => [
                'name'  => 'content',
                '5.2.2' => true,
                '5.2.3' => false,
            ],
        ],
    ];


    /**
     * Bowing out early is not applicable to this sniff.
     *
     * @since 10.0.0
     *
     * @return bool
     */
    protected function bowOutEarly()
    {
        return false;
    }

    /**
     * Process the parameters of a matched function.
     *
     * @since 10.0.0 Part of the logic in this method was previously contained in the
     *               `process()` method (now removed).
     *
     * @param \PHP_CodeSniffer\Files\File $phpcsFile    The file being scanned.
     * @param int                         $stackPtr     The position of the current token in the stack.
     * @param string                      $functionName The token content (function name) which was matched.
     * @param array                       $parameters   Array with information about the parameters.
     *
     * @return void
     */
    public function processParameters(File $phpcsFile, $stackPtr, $functionName, $parameters)
    {
        $functionLc = \strtolower($functionName);

        foreach ($this->targetFunctions[$functionLc] as $offset => $parameterDetails) {
            $targetParam = PassedParameters::getParameterFromStack($parameters, $offset, $parameterDetails['name']);

            if ($targetParam === false) {
                $itemInfo = [
                    'name'   => $functionName,
                    'nameLc' => $functionLc,
                    'offset' => $offset,
                ];
                $this->handleFeature($phpcsFile, $stackPtr, $itemInfo);
            }
        }
    }

    /**
     * Process the function if no parameters were found.
     *
     * @since 10.0.0
     *
     * @param \PHP_CodeSniffer\Files\File $phpcsFile    The file being scanned.
     * @param int                         $stackPtr     The position of the current token in the stack.
     * @param string                      $functionName The token content (function name) which was matched.
     *
     * @return void
     */
    public function processNoParameters(File $phpcsFile, $stackPtr, $functionName)
    {
        $this->processParameters($phpcsFile, $stackPtr, $functionName, []);
    }

    /**
     * Retrieve the relevant detail (version) information for use in an error message.
     *
     * @since 7.1.0
     * @since 10.0.0 - Method renamed from `getErrorInfo()` to `getVersionInfo().
     *               - Second function parameter `$itemInfo` removed.
     *               - Method visibility changed from `public` to `protected`.
     *
     * @param array $itemArray Version and other information about the item.
     *
     * @return array<string, string>
     */
    protected function getVersionInfo(array $itemArray)
    {
        $versionInfo = [
            'requiredVersion' => '',
        ];

        foreach ($itemArray as $version => $required) {
            if (\preg_match('`^\d\.\d(\.\d{1,2})?$`', $version) !== 1) {
                // Not a version key.
                continue;
            }

            if ($required === true && ScannedCode::shouldRunOnOrBelow($version) === true) {
                $versionInfo['requiredVersion'] = $version;
            }
        }

        return $versionInfo;
    }

    /**
     * Handle the retrieval of relevant information and - if necessary - throwing of an
     * error for a matched item.
     *
     * @since 10.0.0 This was previously handled via a similar method in the `AbstractComplexVersionSniff`.
     *
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
     * @param int                         $stackPtr  The position of the relevant token in
     *                                               the stack.
     * @param array                       $itemInfo  Base information about the item.
     *
     * @return void
     */
    protected function handleFeature(File $phpcsFile, $stackPtr, array $itemInfo)
    {
        $itemArray   = $this->targetFunctions[$itemInfo['nameLc']][$itemInfo['offset']];
        $versionInfo = $this->getVersionInfo($itemArray);

        if (empty($versionInfo['requiredVersion'])) {
            return;
        }

        $this->addError($phpcsFile, $stackPtr, $itemInfo, $itemArray, $versionInfo);
    }

    /**
     * Generates the error for this item.
     *
     * @since 7.1.0
     * @since 10.0.0 - Method visibility changed from `public` to `protected`.
     *               - Introduced $itemArray parameter.
     *               - Renamed the last parameter from `$errorInfo` to `$versionInfo`.
     *
     * @param \PHP_CodeSniffer\Files\File $phpcsFile   The file being scanned.
     * @param int                         $stackPtr    The position of the relevant token in
     *                                                 the stack.
     * @param array                       $itemInfo    Base information about the item.
     * @param array                       $itemArray   The sub-array with all the details about
     *                                                 this item.
     * @param array                       $versionInfo Array with detail (version) information
     *                                                 relevant to the item.
     *
     * @return void
     */
    protected function addError(File $phpcsFile, $stackPtr, array $itemInfo, array $itemArray, array $versionInfo)
    {
        $error     = 'The "%s" parameter for function %s() is missing, but was required for PHP version %s and lower';
        $errorCode = MessageHelper::stringToErrorCode($itemInfo['name'] . '_' . $itemArray['name'], true) . 'Missing';
        $data      = [
            $itemArray['name'],
            $itemInfo['name'],
            $versionInfo['requiredVersion'],
        ];

        $phpcsFile->addError($error, $stackPtr, $errorCode, $data);
    }
}
