<?php



declare(strict_types=1);



/**

 * This file is part of phpDocumentor.

 *

 * For the full copyright and license information, please view the LICENSE

 * file that was distributed with this source code.

 *

 * @link      http://phpdoc.org

 */



namespace phpDocumentor\Reflection\Types;



use ArrayIterator;

use InvalidArgumentException;

use ReflectionClass;

use ReflectionClassConstant;

use ReflectionMethod;

use ReflectionParameter;

use ReflectionProperty;

use Reflector;

use RuntimeException;

use UnexpectedValueException;



use function define;

use function defined;

use function file_exists;

use function file_get_contents;

use function get_class;

use function in_array;

use function is_string;

use function strrpos;

use function substr;

use function token_get_all;

use function trim;



use const T_AS;

use const T_CLASS;

use const T_CURLY_OPEN;

use const T_DOLLAR_OPEN_CURLY_BRACES;

use const T_NAME_FULLY_QUALIFIED;

use const T_NAME_QUALIFIED;

use const T_NAMESPACE;

use const T_NS_SEPARATOR;

use const T_STRING;

use const T_USE;



if (!defined('T_NAME_QUALIFIED')) {

    define('T_NAME_QUALIFIED', 'T_NAME_QUALIFIED');

}



if (!defined('T_NAME_FULLY_QUALIFIED')) {

    define('T_NAME_FULLY_QUALIFIED', 'T_NAME_FULLY_QUALIFIED');

}



/**

 * Convenience class to create a Context for DocBlocks when not using the Reflection Component of phpDocumentor.

 *

 * For a DocBlock to be able to resolve types that use partial namespace names or rely on namespace imports we need to

 * provide a bit of context so that the DocBlock can read that and based on it decide how to resolve the types to

 * Fully Qualified names.

 *

 * @see Context for more information.

 */

final class ContextFactory

{

    /** The literal used at the end of a use statement. */

    private const T_LITERAL_END_OF_USE = ';';



    /** The literal used between sets of use statements */

    private const T_LITERAL_USE_SEPARATOR = ',';



    /**

     * Build a Context given a Class Reflection.

     *

     * @see Context for more information on Contexts.

     */

    public function createFromReflector(Reflector $reflector): Context

    {

        if ($reflector instanceof ReflectionClass) {

            //phpcs:ignore SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration.MissingVariable

            /** @var ReflectionClass<object> $reflector */



            return $this->createFromReflectionClass($reflector);

        }



        if ($reflector instanceof ReflectionParameter) {

            return $this->createFromReflectionParameter($reflector);

        }



        if ($reflector instanceof ReflectionMethod) {

            return $this->createFromReflectionMethod($reflector);

        }



        if ($reflector instanceof ReflectionProperty) {

            return $this->createFromReflectionProperty($reflector);

        }



        if ($reflector instanceof ReflectionClassConstant) {

            return $this->createFromReflectionClassConstant($reflector);

        }



        throw new UnexpectedValueException('Unhandled \Reflector instance given:  ' . get_class($reflector));

    }



    private function createFromReflectionParameter(ReflectionParameter $parameter): Context

    {

        $class = $parameter->getDeclaringClass();

        if (!$class) {

            throw new InvalidArgumentException('Unable to get class of ' . $parameter->getName());

        }



        return $this->createFromReflectionClass($class);

    }



    private function createFromReflectionMethod(ReflectionMethod $method): Context

    {

        $class = $method->getDeclaringClass();



        return $this->createFromReflectionClass($class);

    }



    private function createFromReflectionProperty(ReflectionProperty $property): Context

    {

        $class = $property->getDeclaringClass();



        return $this->createFromReflectionClass($class);

    }



    private function createFromReflectionClassConstant(ReflectionClassConstant $constant): Context

    {

        //phpcs:ignore SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration.MissingVariable

        /** @phpstan-var ReflectionClass<object> $class */

        $class = $constant->getDeclaringClass();



        return $this->createFromReflectionClass($class);

    }



    /**

     * @phpstan-param ReflectionClass<object> $class

     */

    private function createFromReflectionClass(ReflectionClass $class): Context

    {

        $fileName  = $class->getFileName();

        $namespace = $class->getNamespaceName();



        if (is_string($fileName) && file_exists($fileName)) {

            $contents = file_get_contents($fileName);

            if ($contents === false) {

                throw new RuntimeException('Unable to read file "' . $fileName . '"');

            }



            return $this->createForNamespace($namespace, $contents);

        }



        return new Context($namespace, []);

    }



    /**

     * Build a Context for a namespace in the provided file contents.

     *

     * @see Context for more information on Contexts.

     *

     * @param string $namespace    It does not matter if a `\` precedes the namespace name,

     * this method first normalizes.

     * @param string $fileContents The file's contents to retrieve the aliases from with the given namespace.

     */

    public function createForNamespace(string $namespace, string $fileContents): Context

    {

        $namespace        = trim($namespace, '\\');

        $useStatements    = [];

        $currentNamespace = '';

        $tokens           = new ArrayIterator(token_get_all($fileContents));



        while ($tokens->valid()) {

            $currentToken = $tokens->current();

            switch ($currentToken[0]) {

                case T_NAMESPACE:

                    $currentNamespace = $this->parseNamespace($tokens);

                    break;

                case T_CLASS:

                    // Fast-forward the iterator through the class so that any

                    // T_USE tokens found within are skipped - these are not

                    // valid namespace use statements so should be ignored.

                    $braceLevel      = 0;

                    $firstBraceFound = false;

                    while ($tokens->valid() && ($braceLevel > 0 || !$firstBraceFound)) {

                        $currentToken = $tokens->current();

                        if (

                            $currentToken === '{'

                            || in_array($currentToken[0], [T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES], true)

                        ) {

                            if (!$firstBraceFound) {

                                $firstBraceFound = true;

                            }



                            ++$braceLevel;

                        }



                        if ($currentToken === '}') {

                            --$braceLevel;

                        }



                        $tokens->next();

                    }



                    break;

                case T_USE:

                    if ($currentNamespace === $namespace) {

                        $useStatements += $this->parseUseStatement($tokens);

                    }



                    break;

            }



            $tokens->next();

        }



        return new Context($namespace, $useStatements);

    }



    /**

     * Deduce the name from tokens when we are at the T_NAMESPACE token.

     *

     * @param ArrayIterator<int, string|array{0:int,1:string,2:int}> $tokens

     */

    private function parseNamespace(ArrayIterator $tokens): string

    {

        // skip to the first string or namespace separator

        $this->skipToNextStringOrNamespaceSeparator($tokens);



        $name = '';

        $acceptedTokens = [T_STRING, T_NS_SEPARATOR, T_NAME_QUALIFIED];

        while ($tokens->valid() && in_array($tokens->current()[0], $acceptedTokens, true)) {

            $name .= $tokens->current()[1];

            $tokens->next();

        }



        return $name;

    }



    /**

     * Deduce the names of all imports when we are at the T_USE token.

     *

     * @param ArrayIterator<int, string|array{0:int,1:string,2:int}> $tokens

     *

     * @return string[]

     * @psalm-return array<string, string>

     */

    private function parseUseStatement(ArrayIterator $tokens): array

    {

        $uses = [];



        while ($tokens->valid()) {

            $this->skipToNextStringOrNamespaceSeparator($tokens);



            $uses += $this->extractUseStatements($tokens);

            $currentToken = $tokens->current();

            if ($currentToken[0] === self::T_LITERAL_END_OF_USE) {

                return $uses;

            }

        }



        return $uses;

    }



    /**

     * Fast-forwards the iterator as longs as we don't encounter a T_STRING or T_NS_SEPARATOR token.

     *

     * @param ArrayIterator<int, string|array{0:int,1:string,2:int}> $tokens

     */

    private function skipToNextStringOrNamespaceSeparator(ArrayIterator $tokens): void

    {

        while ($tokens->valid()) {

            $currentToken = $tokens->current();

            if (in_array($currentToken[0], [T_STRING, T_NS_SEPARATOR], true)) {

                break;

            }



            if ($currentToken[0] === T_NAME_QUALIFIED) {

                break;

            }



            if (defined('T_NAME_FULLY_QUALIFIED') && $currentToken[0] === T_NAME_FULLY_QUALIFIED) {

                break;

            }



            $tokens->next();

        }

    }



    /**

     * Deduce the namespace name and alias of an import when we are at the T_USE token or have not reached the end of

     * a USE statement yet. This will return a key/value array of the alias => namespace.

     *

     * @param ArrayIterator<int, string|array{0:int,1:string,2:int}> $tokens

     *

     * @return string[]

     * @psalm-return array<string, string>

     *

     * @psalm-suppress TypeDoesNotContainType

     */

    private function extractUseStatements(ArrayIterator $tokens): array

    {

        $extractedUseStatements = [];

        $groupedNs              = '';

        $currentNs              = '';

        $currentAlias           = '';

        $state                  = 'start';



        while ($tokens->valid()) {

            $currentToken = $tokens->current();

            $tokenId      = is_string($currentToken) ? $currentToken : $currentToken[0];

            $tokenValue   = is_string($currentToken) ? null : $currentToken[1];

            switch ($state) {

                case 'start':

                    switch ($tokenId) {

                        case T_STRING:

                        case T_NS_SEPARATOR:

                            $currentNs   .= (string) $tokenValue;

                            $currentAlias =  $tokenValue;

                            break;

                        case T_NAME_QUALIFIED:

                        case T_NAME_FULLY_QUALIFIED:

                            $currentNs   .= (string) $tokenValue;

                            $currentAlias = substr(

                                (string) $tokenValue,

                                (int) (strrpos((string) $tokenValue, '\\')) + 1

                            );

                            break;

                        case T_CURLY_OPEN:

                        case '{':

                            $state     = 'grouped';

                            $groupedNs = $currentNs;

                            break;

                        case T_AS:

                            $state = 'start-alias';

                            break;

                        case self::T_LITERAL_USE_SEPARATOR:

                        case self::T_LITERAL_END_OF_USE:

                            $state = 'end';

                            break;

                        default:

                            break;

                    }



                    break;

                case 'start-alias':

                    switch ($tokenId) {

                        case T_STRING:

                            $currentAlias = $tokenValue;

                            break;

                        case self::T_LITERAL_USE_SEPARATOR:

                        case self::T_LITERAL_END_OF_USE:

                            $state = 'end';

                            break;

                        default:

                            break;

                    }



                    break;

                case 'grouped':

                    switch ($tokenId) {

                        case T_STRING:

                        case T_NS_SEPARATOR:

                            $currentNs   .= (string) $tokenValue;

                            $currentAlias = $tokenValue;

                            break;

                        case T_AS:

                            $state = 'grouped-alias';

                            break;

                        case self::T_LITERAL_USE_SEPARATOR:

                            $state                                          = 'grouped';

                            $extractedUseStatements[(string) $currentAlias] = $currentNs;

                            $currentNs                                      = $groupedNs;

                            $currentAlias                                   = '';

                            break;

                        case self::T_LITERAL_END_OF_USE:

                            $state = 'end';

                            break;

                        default:

                            break;

                    }



                    break;

                case 'grouped-alias':

                    switch ($tokenId) {

                        case T_STRING:

                            $currentAlias = $tokenValue;

                            break;

                        case self::T_LITERAL_USE_SEPARATOR:

                            $state                                          = 'grouped';

                            $extractedUseStatements[(string) $currentAlias] = $currentNs;

                            $currentNs                                      = $groupedNs;

                            $currentAlias                                   = '';

                            break;

                        case self::T_LITERAL_END_OF_USE:

                            $state = 'end';

                            break;

                        default:

                            break;

                    }

            }



            if ($state === 'end') {

                break;

            }



            $tokens->next();

        }



        if ($groupedNs !== $currentNs) {

            $extractedUseStatements[(string) $currentAlias] = $currentNs;

        }



        return $extractedUseStatements;

    }

}

