<?php
/**
 * PHPCS cross-version compatibility helper class.
 *
 * @category PHP
 * @package  PHPCompatibility
 * @author   Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
 */

namespace PHPCompatibility;

use PHP_CodeSniffer_Exception as PHPCS_Exception;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;

/**
 * \PHPCompatibility\PHPCSHelper
 *
 * PHPCS cross-version compatibility helper class.
 *
 * A number of PHPCS classes were split up into several classes in PHPCS 3.x
 * Those classes cannot be aliased as they don't represent the same object.
 * This class provides helper methods for functions which were contained in
 * one of these classes and which are used within the PHPCompatibility library.
 *
 * @category PHP
 * @package  PHPCompatibility
 * @author   Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
 */
class PHPCSHelper
{

    /**
     * Get the PHPCS version number.
     *
     * @return string
     */
    public static function getVersion()
    {
        if (defined('\PHP_CodeSniffer\Config::VERSION')) {
            // PHPCS 3.x.
            return \PHP_CodeSniffer\Config::VERSION;
        } else {
            // PHPCS 2.x.
            return \PHP_CodeSniffer::VERSION;
        }
    }


    /**
     * Pass config data to PHPCS.
     *
     * PHPCS cross-version compatibility helper.
     *
     * @param string      $key   The name of the config value.
     * @param string|null $value The value to set. If null, the config entry
     *                           is deleted, reverting it to the default value.
     * @param boolean     $temp  Set this config data temporarily for this script run.
     *                           This will not write the config data to the config file.
     *
     * @return void
     */
    public static function setConfigData($key, $value, $temp = false)
    {
        if (method_exists('\PHP_CodeSniffer\Config', 'setConfigData')) {
            // PHPCS 3.x.
            \PHP_CodeSniffer\Config::setConfigData($key, $value, $temp);
        } else {
            // PHPCS 2.x.
            \PHP_CodeSniffer::setConfigData($key, $value, $temp);
        }
    }


    /**
     * Get the value of a single PHPCS config key.
     *
     * @param string $key The name of the config value.
     *
     * @return string|null
     */
    public static function getConfigData($key)
    {
        if (method_exists('\PHP_CodeSniffer\Config', 'getConfigData')) {
            // PHPCS 3.x.
            return \PHP_CodeSniffer\Config::getConfigData($key);
        } else {
            // PHPCS 2.x.
            return \PHP_CodeSniffer::getConfigData($key);
        }
    }


    /**
     * Get the value of a single PHPCS config key.
     *
     * This config key can be set in the `CodeSniffer.conf` file, on the
     * command-line or in a ruleset.
     *
     * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
     * @param string                $key       The name of the config value.
     *
     * @return string|null
     */
    public static function getCommandLineData(File $phpcsFile, $key)
    {
        if (class_exists('\PHP_CodeSniffer\Config')) {
            // PHPCS 3.x.
            $config = $phpcsFile->config;
            if (isset($config->{$key})) {
                return $config->{$key};
            }
        } else {
            // PHPCS 2.x.
            $config = $phpcsFile->phpcs->cli->getCommandLineValues();
            if (isset($config[$key])) {
                return $config[$key];
            }
        }

        return null;
    }


    /**
     * Returns the position of the first non-whitespace token in a statement.
     *
     * {@internal Duplicate of same method as contained in the `\PHP_CodeSniffer_File`
     * class and introduced in PHPCS 2.1.0 and improved in PHPCS 2.7.1.
     *
     * Once the minimum supported PHPCS version for this standard goes beyond
     * that, this method can be removed and calls to it replaced with
     * `$phpcsFile->findStartOfStatement($start, $ignore)` calls.
     *
     * Last synced with PHPCS version: PHPCS 3.3.2 at commit 6ad28354c04b364c3c71a34e4a18b629cc3b231e}}
     *
     * @param \PHP_CodeSniffer_File $phpcsFile Instance of phpcsFile.
     * @param int                   $start     The position to start searching from in the token stack.
     * @param int|array             $ignore    Token types that should not be considered stop points.
     *
     * @return int
     */
    public static function findStartOfStatement(File $phpcsFile, $start, $ignore = null)
    {
        if (version_compare(self::getVersion(), '2.7.1', '>=') === true) {
            return $phpcsFile->findStartOfStatement($start, $ignore);
        }

        $tokens    = $phpcsFile->getTokens();
        $endTokens = Tokens::$blockOpeners;

        $endTokens[T_COLON]            = true;
        $endTokens[T_COMMA]            = true;
        $endTokens[T_DOUBLE_ARROW]     = true;
        $endTokens[T_SEMICOLON]        = true;
        $endTokens[T_OPEN_TAG]         = true;
        $endTokens[T_CLOSE_TAG]        = true;
        $endTokens[T_OPEN_SHORT_ARRAY] = true;

        if ($ignore !== null) {
            $ignore = (array) $ignore;
            foreach ($ignore as $code) {
                if (isset($endTokens[$code]) === true) {
                    unset($endTokens[$code]);
                }
            }
        }

        $lastNotEmpty = $start;

        for ($i = $start; $i >= 0; $i--) {
            if (isset($endTokens[$tokens[$i]['code']]) === true) {
                // Found the end of the previous statement.
                return $lastNotEmpty;
            }

            if (isset($tokens[$i]['scope_opener']) === true
                && $i === $tokens[$i]['scope_closer']
            ) {
                // Found the end of the previous scope block.
                return $lastNotEmpty;
            }

            // Skip nested statements.
            if (isset($tokens[$i]['bracket_opener']) === true
                && $i === $tokens[$i]['bracket_closer']
            ) {
                $i = $tokens[$i]['bracket_opener'];
            } elseif (isset($tokens[$i]['parenthesis_opener']) === true
                && $i === $tokens[$i]['parenthesis_closer']
            ) {
                $i = $tokens[$i]['parenthesis_opener'];
            }

            if (isset(Tokens::$emptyTokens[$tokens[$i]['code']]) === false) {
                $lastNotEmpty = $i;
            }
        }//end for

        return 0;
    }


    /**
     * Returns the position of the last non-whitespace token in a statement.
     *
     * {@internal Duplicate of same method as contained in the `\PHP_CodeSniffer_File`
     * class and introduced in PHPCS 2.1.0 and improved in PHPCS 2.7.1 and 3.3.0.
     *
     * Once the minimum supported PHPCS version for this standard goes beyond
     * that, this method can be removed and calls to it replaced with
     * `$phpcsFile->findEndOfStatement($start, $ignore)` calls.
     *
     * Last synced with PHPCS version: PHPCS 3.3.0-alpha at commit f5d899dcb5c534a1c3cca34668624517856ba823}}
     *
     * @param \PHP_CodeSniffer_File $phpcsFile Instance of phpcsFile.
     * @param int                   $start     The position to start searching from in the token stack.
     * @param int|array             $ignore    Token types that should not be considered stop points.
     *
     * @return int
     */
    public static function findEndOfStatement(File $phpcsFile, $start, $ignore = null)
    {
        if (version_compare(self::getVersion(), '3.3.0', '>=') === true) {
            return $phpcsFile->findEndOfStatement($start, $ignore);
        }

        $tokens    = $phpcsFile->getTokens();
        $endTokens = array(
            T_COLON                => true,
            T_COMMA                => true,
            T_DOUBLE_ARROW         => true,
            T_SEMICOLON            => true,
            T_CLOSE_PARENTHESIS    => true,
            T_CLOSE_SQUARE_BRACKET => true,
            T_CLOSE_CURLY_BRACKET  => true,
            T_CLOSE_SHORT_ARRAY    => true,
            T_OPEN_TAG             => true,
            T_CLOSE_TAG            => true,
        );

        if ($ignore !== null) {
            $ignore = (array) $ignore;
            foreach ($ignore as $code) {
                if (isset($endTokens[$code]) === true) {
                    unset($endTokens[$code]);
                }
            }
        }

        $lastNotEmpty = $start;

        for ($i = $start; $i < $phpcsFile->numTokens; $i++) {
            if ($i !== $start && isset($endTokens[$tokens[$i]['code']]) === true) {
                // Found the end of the statement.
                if ($tokens[$i]['code'] === T_CLOSE_PARENTHESIS
                    || $tokens[$i]['code'] === T_CLOSE_SQUARE_BRACKET
                    || $tokens[$i]['code'] === T_CLOSE_CURLY_BRACKET
                    || $tokens[$i]['code'] === T_CLOSE_SHORT_ARRAY
                    || $tokens[$i]['code'] === T_OPEN_TAG
                    || $tokens[$i]['code'] === T_CLOSE_TAG
                ) {
                    return $lastNotEmpty;
                }

                return $i;
            }

            // Skip nested statements.
            if (isset($tokens[$i]['scope_closer']) === true
                && ($i === $tokens[$i]['scope_opener']
                || $i === $tokens[$i]['scope_condition'])
            ) {
                if ($i === $start && isset(Tokens::$scopeOpeners[$tokens[$i]['code']]) === true) {
                    return $tokens[$i]['scope_closer'];
                }

                $i = $tokens[$i]['scope_closer'];
            } elseif (isset($tokens[$i]['bracket_closer']) === true
                && $i === $tokens[$i]['bracket_opener']
            ) {
                $i = $tokens[$i]['bracket_closer'];
            } elseif (isset($tokens[$i]['parenthesis_closer']) === true
                && $i === $tokens[$i]['parenthesis_opener']
            ) {
                $i = $tokens[$i]['parenthesis_closer'];
            }

            if (isset(Tokens::$emptyTokens[$tokens[$i]['code']]) === false) {
                $lastNotEmpty = $i;
            }
        }//end for

        return ($phpcsFile->numTokens - 1);
    }


    /**
     * Returns the name of the class that the specified class extends
     * (works for classes, anonymous classes and interfaces).
     *
     * Returns FALSE on error or if there is no extended class name.
     *
     * {@internal Duplicate of same method as contained in the `\PHP_CodeSniffer_File`
     * class, but with some improvements which have been introduced in
     * PHPCS 2.8.0.
     * {@link https://github.com/squizlabs/PHP_CodeSniffer/commit/0011d448119d4c568e3ac1f825ae78815bf2cc34}.
     *
     * Once the minimum supported PHPCS version for this standard goes beyond
     * that, this method can be removed and calls to it replaced with
     * `$phpcsFile->findExtendedClassName($stackPtr)` calls.
     *
     * Last synced with PHPCS version: PHPCS 3.1.0-alpha at commit a9efcc9b0703f3f9f4a900623d4e97128a6aafc6}}
     *
     * @param \PHP_CodeSniffer_File $phpcsFile Instance of phpcsFile.
     * @param int                   $stackPtr  The position of the class token in the stack.
     *
     * @return string|false
     */
    public static function findExtendedClassName(File $phpcsFile, $stackPtr)
    {
        if (version_compare(self::getVersion(), '3.1.0', '>=') === true) {
            return $phpcsFile->findExtendedClassName($stackPtr);
        }

        $tokens = $phpcsFile->getTokens();

        // Check for the existence of the token.
        if (isset($tokens[$stackPtr]) === false) {
            return false;
        }

        if ($tokens[$stackPtr]['code'] !== T_CLASS
            && $tokens[$stackPtr]['type'] !== 'T_ANON_CLASS'
            && $tokens[$stackPtr]['type'] !== 'T_INTERFACE'
        ) {
            return false;
        }

        if (isset($tokens[$stackPtr]['scope_closer']) === false) {
            return false;
        }

        $classCloserIndex = $tokens[$stackPtr]['scope_closer'];
        $extendsIndex     = $phpcsFile->findNext(T_EXTENDS, $stackPtr, $classCloserIndex);
        if ($extendsIndex === false) {
            return false;
        }

        $find = array(
            T_NS_SEPARATOR,
            T_STRING,
            T_WHITESPACE,
        );

        $end  = $phpcsFile->findNext($find, ($extendsIndex + 1), $classCloserIndex, true);
        $name = $phpcsFile->getTokensAsString(($extendsIndex + 1), ($end - $extendsIndex - 1));
        $name = trim($name);

        if ($name === '') {
            return false;
        }

        return $name;
    }


    /**
     * Returns the name(s) of the interface(s) that the specified class implements.
     *
     * Returns FALSE on error or if there are no implemented interface names.
     *
     * {@internal Duplicate of same method as introduced in PHPCS 2.7.
     * This method also includes an improvement we use which was only introduced
     * in PHPCS 2.8.0, so only defer to upstream for higher versions.
     * Once the minimum supported PHPCS version for this sniff library goes beyond
     * that, this method can be removed and calls to it replaced with
     * `$phpcsFile->findImplementedInterfaceNames($stackPtr)` calls.}}
     *
     * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
     * @param int                   $stackPtr  The position of the class token.
     *
     * @return array|false
     */
    public static function findImplementedInterfaceNames(File $phpcsFile, $stackPtr)
    {
        if (version_compare(self::getVersion(), '2.7.1', '>') === true) {
            return $phpcsFile->findImplementedInterfaceNames($stackPtr);
        }

        $tokens = $phpcsFile->getTokens();

        // Check for the existence of the token.
        if (isset($tokens[$stackPtr]) === false) {
            return false;
        }

        if ($tokens[$stackPtr]['code'] !== T_CLASS
            && $tokens[$stackPtr]['type'] !== 'T_ANON_CLASS'
        ) {
            return false;
        }

        if (isset($tokens[$stackPtr]['scope_closer']) === false) {
            return false;
        }

        $classOpenerIndex = $tokens[$stackPtr]['scope_opener'];
        $implementsIndex  = $phpcsFile->findNext(T_IMPLEMENTS, $stackPtr, $classOpenerIndex);
        if ($implementsIndex === false) {
            return false;
        }

        $find = array(
            T_NS_SEPARATOR,
            T_STRING,
            T_WHITESPACE,
            T_COMMA,
        );

        $end  = $phpcsFile->findNext($find, ($implementsIndex + 1), ($classOpenerIndex + 1), true);
        $name = $phpcsFile->getTokensAsString(($implementsIndex + 1), ($end - $implementsIndex - 1));
        $name = trim($name);

        if ($name === '') {
            return false;
        } else {
            $names = explode(',', $name);
            $names = array_map('trim', $names);
            return $names;
        }
    }


    /**
     * Returns the method parameters for the specified function token.
     *
     * Each parameter is in the following format:
     *
     * <code>
     *   0 => array(
     *         'name'              => '$var',  // The variable name.
     *         'token'             => integer, // The stack pointer to the variable name.
     *         'content'           => string,  // The full content of the variable definition.
     *         'pass_by_reference' => boolean, // Is the variable passed by reference?
     *         'variable_length'   => boolean, // Is the param of variable length through use of `...` ?
     *         'type_hint'         => string,  // The type hint for the variable.
     *         'type_hint_token'   => integer, // The stack pointer to the type hint
     *                                         // or false if there is no type hint.
     *         'nullable_type'     => boolean, // Is the variable using a nullable type?
     *        )
     * </code>
     *
     * Parameters with default values have an additional array index of
     * 'default' with the value of the default as a string.
     *
     * {@internal Duplicate of same method as contained in the `\PHP_CodeSniffer_File`
     * class.
     *
     * Last synced with PHPCS version: PHPCS 3.3.0-alpha at commit 53a28408d345044c0360c2c1b4a2aaebf4a3b8c9}}
     *
     * @param \PHP_CodeSniffer_File $phpcsFile Instance of phpcsFile.
     * @param int                   $stackPtr  The position in the stack of the
     *                                         function token to acquire the
     *                                         parameters for.
     *
     * @return array|false
     * @throws \PHP_CodeSniffer_Exception If the specified $stackPtr is not of
     *                                    type T_FUNCTION or T_CLOSURE.
     */
    public static function getMethodParameters(File $phpcsFile, $stackPtr)
    {
        if (version_compare(self::getVersion(), '3.3.0', '>=') === true) {
            return $phpcsFile->getMethodParameters($stackPtr);
        }

        $tokens = $phpcsFile->getTokens();

        // Check for the existence of the token.
        if (isset($tokens[$stackPtr]) === false) {
            return false;
        }

        if ($tokens[$stackPtr]['code'] !== T_FUNCTION
            && $tokens[$stackPtr]['code'] !== T_CLOSURE
        ) {
            throw new PHPCS_Exception('$stackPtr must be of type T_FUNCTION or T_CLOSURE');
        }

        $opener = $tokens[$stackPtr]['parenthesis_opener'];
        $closer = $tokens[$stackPtr]['parenthesis_closer'];

        $vars            = array();
        $currVar         = null;
        $paramStart      = ($opener + 1);
        $defaultStart    = null;
        $paramCount      = 0;
        $passByReference = false;
        $variableLength  = false;
        $typeHint        = '';
        $typeHintToken   = false;
        $nullableType    = false;

        for ($i = $paramStart; $i <= $closer; $i++) {
            // Check to see if this token has a parenthesis or bracket opener. If it does
            // it's likely to be an array which might have arguments in it. This
            // could cause problems in our parsing below, so lets just skip to the
            // end of it.
            if (isset($tokens[$i]['parenthesis_opener']) === true) {
                // Don't do this if it's the close parenthesis for the method.
                if ($i !== $tokens[$i]['parenthesis_closer']) {
                    $i = ($tokens[$i]['parenthesis_closer'] + 1);
                }
            }

            if (isset($tokens[$i]['bracket_opener']) === true) {
                // Don't do this if it's the close parenthesis for the method.
                if ($i !== $tokens[$i]['bracket_closer']) {
                    $i = ($tokens[$i]['bracket_closer'] + 1);
                }
            }

            switch ($tokens[$i]['type']) {
                case 'T_BITWISE_AND':
                    if ($defaultStart === null) {
                        $passByReference = true;
                    }
                    break;
                case 'T_VARIABLE':
                    $currVar = $i;
                    break;
                case 'T_ELLIPSIS':
                    $variableLength = true;
                    break;
                case 'T_ARRAY_HINT': // Pre-PHPCS 3.3.0.
                case 'T_CALLABLE':
                    if ($typeHintToken === false) {
                        $typeHintToken = $i;
                    }

                    $typeHint .= $tokens[$i]['content'];
                    break;
                case 'T_SELF':
                case 'T_PARENT':
                case 'T_STATIC':
                    // Self and parent are valid, static invalid, but was probably intended as type hint.
                    if (isset($defaultStart) === false) {
                        if ($typeHintToken === false) {
                            $typeHintToken = $i;
                        }

                        $typeHint .= $tokens[$i]['content'];
                    }
                    break;
                case 'T_STRING':
                    // This is a string, so it may be a type hint, but it could
                    // also be a constant used as a default value.
                    $prevComma = false;
                    for ($t = $i; $t >= $opener; $t--) {
                        if ($tokens[$t]['code'] === T_COMMA) {
                            $prevComma = $t;
                            break;
                        }
                    }

                    if ($prevComma !== false) {
                        $nextEquals = false;
                        for ($t = $prevComma; $t < $i; $t++) {
                            if ($tokens[$t]['code'] === T_EQUAL) {
                                $nextEquals = $t;
                                break;
                            }
                        }

                        if ($nextEquals !== false) {
                            break;
                        }
                    }

                    if ($defaultStart === null) {
                        if ($typeHintToken === false) {
                            $typeHintToken = $i;
                        }

                        $typeHint .= $tokens[$i]['content'];
                    }
                    break;
                case 'T_NS_SEPARATOR':
                    // Part of a type hint or default value.
                    if ($defaultStart === null) {
                        if ($typeHintToken === false) {
                            $typeHintToken = $i;
                        }

                        $typeHint .= $tokens[$i]['content'];
                    }
                    break;
                case 'T_NULLABLE':
                case 'T_INLINE_THEN': // Pre-PHPCS 2.8.0.
                    if ($defaultStart === null) {
                        $nullableType = true;
                        $typeHint    .= $tokens[$i]['content'];
                    }
                    break;
                case 'T_CLOSE_PARENTHESIS':
                case 'T_COMMA':
                    // If it's null, then there must be no parameters for this
                    // method.
                    if ($currVar === null) {
                        break;
                    }

                    $vars[$paramCount]            = array();
                    $vars[$paramCount]['token']   = $currVar;
                    $vars[$paramCount]['name']    = $tokens[$currVar]['content'];
                    $vars[$paramCount]['content'] = trim($phpcsFile->getTokensAsString($paramStart, ($i - $paramStart)));

                    if ($defaultStart !== null) {
                        $vars[$paramCount]['default'] = trim(
                            $phpcsFile->getTokensAsString(
                                $defaultStart,
                                ($i - $defaultStart)
                            )
                        );
                    }

                    $vars[$paramCount]['pass_by_reference'] = $passByReference;
                    $vars[$paramCount]['variable_length']   = $variableLength;
                    $vars[$paramCount]['type_hint']         = $typeHint;
                    $vars[$paramCount]['type_hint_token']   = $typeHintToken;
                    $vars[$paramCount]['nullable_type']     = $nullableType;

                    // Reset the vars, as we are about to process the next parameter.
                    $defaultStart    = null;
                    $paramStart      = ($i + 1);
                    $passByReference = false;
                    $variableLength  = false;
                    $typeHint        = '';
                    $typeHintToken   = false;
                    $nullableType    = false;

                    $paramCount++;
                    break;
                case 'T_EQUAL':
                    $defaultStart = ($i + 1);
                    break;
            }//end switch
        }//end for

        return $vars;
    }
}
