<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */

namespace Magento\Setup\Module\Di\Code\Reader;

use Laminas\Code\Exception\InvalidArgumentException;
use Laminas\Code\Exception\RuntimeException;

/**
 * FileScanner code reader
 *
 * @SuppressWarnings(PHPMD)
 */
class FileScanner
{
    /**
     * @var string
     */
    protected $file;

    /**
     * @var bool
     */
    protected $isScanned = false;

    /**
     * @var array
     */
    protected $tokens = [];

    /**
     * @var array
     */
    protected $infos = [];

    /**
     * @var int
     */
    private $tokenType;

    /**
     * copied from laminas-code 3.5.1
     *
     * @param string $file
     *
     * @throws InvalidArgumentException
     */
    public function __construct(string $file)
    {
        $this->file = $file;
        // phpcs:ignore Magento2.Functions.DiscouragedFunction
        if (!file_exists($file)) {
            throw new InvalidArgumentException(sprintf(
                'File "%s" not found',
                $file
            ));
        }
        // phpcs:ignore Magento2.Functions.DiscouragedFunction
        $tokens = token_get_all(file_get_contents($file));
        $this->tokens = $tokens;
    }

    /**
     * @inheritDoc
     */
    protected function scan()
    {
        if ($this->isScanned) {
            return;
        }

        if (!$this->tokens) {
            throw new RuntimeException('No tokens were provided');
        }

        /**
         * Define PHP 5.4 'trait' token constant.
         */
        if (!defined('T_TRAIT')) {
            define('T_TRAIT', 42001);
        }

        $namespaceContentTokenTypes = [
            T_NS_SEPARATOR => T_NS_SEPARATOR,
            T_STRING => T_STRING,
            T_NAME_QUALIFIED => T_NAME_QUALIFIED,
            T_NAME_FULLY_QUALIFIED => T_NAME_FULLY_QUALIFIED
        ];

        /**
         * Variables & Setup
         */

        $tokens = &$this->tokens; // localize
        $infos = &$this->infos; // localize
        $tokenIndex = null;
        $token = null;
        $this->tokenType = null;
        $tokenContent = null;
        $tokenLine = null;
        $namespace = null;
        $docCommentIndex = false;
        $infoIndex = 0;

        /*
         * MACRO creation
         */
        $macroTokenAdvance = function () use (&$tokens, &$tokenIndex, &$token, &$tokenContent, &$tokenLine) {
            $tokenIndex = ($tokenIndex === null) ? 0 : $tokenIndex + 1;
            if (!isset($tokens[$tokenIndex])) {
                $token = false;
                $tokenContent = false;
                $this->tokenType = false;
                $tokenLine = false;

                return false;
            }
            if (is_string($tokens[$tokenIndex]) && $tokens[$tokenIndex] === '"') {
                do {
                    $tokenIndex++;
                } while (!(is_string($tokens[$tokenIndex]) && $tokens[$tokenIndex] === '"'));
            }
            $token = $tokens[$tokenIndex];
            if (is_array($token)) {
                list($this->tokenType, $tokenContent, $tokenLine) = $token;
            } else {
                $this->tokenType = null;
                $tokenContent = $token;
            }

            return $tokenIndex;
        };
        $macroTokenLogicalStartIndex = function () use (&$tokenIndex, &$docCommentIndex) {
            return ($docCommentIndex === false) ? $tokenIndex : $docCommentIndex;
        };
        $macroDocCommentStart = function () use (&$tokenIndex, &$docCommentIndex) {
            $docCommentIndex = $tokenIndex;

            return $docCommentIndex;
        };
        $macroDocCommentValidate = function () use (&$docCommentIndex) {
            static $validTrailingTokens = null;
            if ($validTrailingTokens === null) {
                $validTrailingTokens = [T_WHITESPACE, T_FINAL, T_ABSTRACT, T_INTERFACE, T_CLASS, T_FUNCTION];
            }
            if ($docCommentIndex !== false && !in_array($this->tokenType, $validTrailingTokens)) {
                $docCommentIndex = false;
            }

            return $docCommentIndex;
        };
        $macroInfoAdvance = function () use (&$infoIndex, &$infos, &$tokenIndex, &$tokenLine) {
            $infos[$infoIndex]['tokenEnd'] = $tokenIndex;
            $infos[$infoIndex]['lineEnd'] = $tokenLine;
            $infoIndex++;

            return $infoIndex;
        };

        // phpcs:disable
        /**
         * START FINITE STATE MACHINE FOR SCANNING TOKENS
         */

        // Initialize token
        $macroTokenAdvance();

        SCANNER_TOP:

        if ($token === false) {
            goto SCANNER_END;
        }

        // Validate current doc comment index
        $macroDocCommentValidate();

        switch ($this->tokenType) {
            case T_DOC_COMMENT:
                $macroDocCommentStart();
                goto SCANNER_CONTINUE;
            //goto no break needed

            case T_NAMESPACE:
                $infos[$infoIndex] = [
                    'type' => 'namespace',
                    'tokenStart' => $macroTokenLogicalStartIndex(),
                    'tokenEnd' => null,
                    'lineStart' => $token[2],
                    'lineEnd' => null,
                    'namespace' => null,
                ];

                // start processing with next token
                if ($macroTokenAdvance() === false) {
                    goto SCANNER_END;
                }

                SCANNER_NAMESPACE_TOP:

                if ($this->tokenType === null && $tokenContent === ';' || $tokenContent === '{') {
                    goto SCANNER_NAMESPACE_END;
                }

                if ($this->tokenType === T_WHITESPACE) {
                    goto SCANNER_NAMESPACE_CONTINUE;
                }
                if (isset($namespaceContentTokenTypes[$this->tokenType])) {
                    $infos[$infoIndex]['namespace'] .= $tokenContent;
                }

                SCANNER_NAMESPACE_CONTINUE:

                if ($macroTokenAdvance() === false) {
                    goto SCANNER_END;
                }
                goto SCANNER_NAMESPACE_TOP;

                SCANNER_NAMESPACE_END:

                $namespace = $infos[$infoIndex]['namespace'];

                $macroInfoAdvance();
                goto SCANNER_CONTINUE;
            //goto no break needed

            case T_USE:
                $infos[$infoIndex] = [
                    'type' => 'use',
                    'tokenStart' => $macroTokenLogicalStartIndex(),
                    'tokenEnd' => null,
                    'lineStart' => $tokens[$tokenIndex][2],
                    'lineEnd' => null,
                    'namespace' => $namespace,
                    'statements' => [0 => ['use' => null, 'as' => null]],
                ];

                $useStatementIndex = 0;
                $useAsContext = false;

                // start processing with next token
                if ($macroTokenAdvance() === false) {
                    goto SCANNER_END;
                }

                SCANNER_USE_TOP:

                if ($this->tokenType === null) {
                    if ($tokenContent === ';') {
                        goto SCANNER_USE_END;
                    } elseif ($tokenContent === ',') {
                        $useAsContext = false;
                        $useStatementIndex++;
                        $infos[$infoIndex]['statements'][$useStatementIndex] = ['use' => null, 'as' => null];
                    }
                }

                // ANALYZE
                if ($this->tokenType !== null) {
                    if ($this->tokenType == T_AS) {
                        $useAsContext = true;
                        goto SCANNER_USE_CONTINUE;
                    }

                    if (\array_key_exists($this->tokenType, $namespaceContentTokenTypes)) {
                        if ($useAsContext == false) {
                            $infos[$infoIndex]['statements'][$useStatementIndex]['use'] .= $tokenContent;
                        } else {
                            $infos[$infoIndex]['statements'][$useStatementIndex]['as'] = $tokenContent;
                        }
                    }
                }

                SCANNER_USE_CONTINUE:

                if ($macroTokenAdvance() === false) {
                    goto SCANNER_END;
                }
                goto SCANNER_USE_TOP;

                SCANNER_USE_END:

                $macroInfoAdvance();
                goto SCANNER_CONTINUE;
            //goto no break needed

            case T_INCLUDE:
            case T_INCLUDE_ONCE:
            case T_REQUIRE:
            case T_REQUIRE_ONCE:
                // Static for performance
                static $includeTypes = [
                    T_INCLUDE => 'include',
                    T_INCLUDE_ONCE => 'include_once',
                    T_REQUIRE => 'require',
                    T_REQUIRE_ONCE => 'require_once'
                ];

                $infos[$infoIndex] = [
                    'type' => 'include',
                    'tokenStart' => $macroTokenLogicalStartIndex(),
                    'tokenEnd' => null,
                    'lineStart' => $tokens[$tokenIndex][2],
                    'lineEnd' => null,
                    'includeType' => $includeTypes[$tokens[$tokenIndex][0]],
                    'path' => '',
                ];

                // start processing with next token
                if ($macroTokenAdvance() === false) {
                    goto SCANNER_END;
                }

                SCANNER_INCLUDE_TOP:

                if ($this->tokenType === null && $tokenContent === ';') {
                    goto SCANNER_INCLUDE_END;
                }

                $infos[$infoIndex]['path'] .= $tokenContent;

                SCANNER_INCLUDE_CONTINUE:

                if ($macroTokenAdvance() === false) {
                    goto SCANNER_END;
                }
                goto SCANNER_INCLUDE_TOP;

                SCANNER_INCLUDE_END:

                $macroInfoAdvance();
                goto SCANNER_CONTINUE;
            //goto no break needed

            case T_FUNCTION:
            case T_FINAL:
            case T_ABSTRACT:
            case T_CLASS:
            case T_INTERFACE:
            case T_TRAIT:
                $infos[$infoIndex] = [
                    'type' => ($this->tokenType === T_FUNCTION) ? 'function' : 'class',
                    'tokenStart' => $macroTokenLogicalStartIndex(),
                    'tokenEnd' => null,
                    'lineStart' => $tokens[$tokenIndex][2],
                    'lineEnd' => null,
                    'namespace' => $namespace,
                    'uses' => $this->getUsesNoScan($namespace),
                    'name' => null,
                    'shortName' => null,
                ];

                $classBraceCount = 0;

                // start processing with current token

                SCANNER_CLASS_TOP:

                // process the name
                if ($infos[$infoIndex]['shortName'] == ''
                    && (($this->tokenType === T_CLASS
                            || $this->tokenType === T_INTERFACE
                            || $this->tokenType === T_TRAIT
                        )
                        && $infos[$infoIndex]['type'] === 'class' && $tokens[$tokenIndex - 1][0] !== T_DOUBLE_COLON
                        || ($this->tokenType === T_FUNCTION && $infos[$infoIndex]['type'] === 'function'))
                ) {
                    $infos[$infoIndex]['shortName'] = $tokens[$tokenIndex + 2][1];
                    $infos[$infoIndex]['name'] = (($namespace !== null)
                            ? $namespace . '\\'
                            : '') . $infos[$infoIndex]['shortName'];
                }

                if ($this->tokenType === null) {
                    if ($tokenContent === '{') {
                        $classBraceCount++;
                    }
                    if ($tokenContent === '}') {
                        $classBraceCount--;
                        if ($classBraceCount === 0) {
                            goto SCANNER_CLASS_END;
                        }
                    }
                }

                SCANNER_CLASS_CONTINUE:

                if ($macroTokenAdvance() === false) {
                    goto SCANNER_END;
                }
                goto SCANNER_CLASS_TOP;

                SCANNER_CLASS_END:

                $macroInfoAdvance();
                goto SCANNER_CONTINUE;
        }

        SCANNER_CONTINUE:

        if ($macroTokenAdvance() === false) {
            goto SCANNER_END;
        }
        goto SCANNER_TOP;

        SCANNER_END:

        /**
         * END FINITE STATE MACHINE FOR SCANNING TOKENS
         */
        $this->isScanned = true;
        // phpcs:enable
    }

    /**
     * Copied from laminas-code 3.5.1
     *
     * @param string|null $namespace
     *
     * @return array|null
     */
    public function getUses(string $namespace = null): ?array
    {
        $this->scan();

        return $this->getUsesNoScan($namespace);
    }

    /**
     * Copied from laminas-code 3.5.1
     *
     * @param string|null $namespace
     *
     * @return array|null
     */
    protected function getUsesNoScan(string $namespace = null): ?array
    {
        $namespaces = [];
        foreach ($this->infos as $info) {
            if ($info['type'] === 'namespace') {
                $namespaces[] = $info['namespace'];
            }
        }

        if ($namespace === null) {
            $namespace = array_shift($namespaces);
        } elseif (!in_array($namespace, $namespaces, true)) {
            return null;
        }

        $uses = [];
        foreach ($this->infos as $info) {
            if ($info['type'] !== 'use') {
                continue;
            }
            foreach ($info['statements'] as $statement) {
                if ($info['namespace'] === $namespace) {
                    $uses[] = $statement;
                }
            }
        }

        return $uses;
    }
}
