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

namespace Magento\Framework\App\Router;

use Magento\Framework\App\ActionInterface;
use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\App\State;
use Magento\Framework\App\Utility\ReflectionClassFactory;
use Magento\Framework\Config\CacheInterface;
use Magento\Framework\Exception\FileSystemException;
use Magento\Framework\Module\Dir\Reader as ModuleReader;
use Magento\Framework\Serialize\Serializer\Serialize;
use Magento\Framework\Serialize\SerializerInterface;
use ReflectionClass;
use ReflectionException;

/**
 * Class to retrieve action class.
 */
class ActionList
{
    /**
     * Not allowed string in route's action path to avoid disclosing admin url
     */
    public const NOT_ALLOWED_IN_NAMESPACE_PATH = 'adminhtml';

    /**
     * List of application actions
     *
     * @var array
     */
    protected $actions;

    /**
     * @var array
     */
    protected $reservedWords = [
        'abstract', 'and', 'array', 'as', 'break', 'callable', 'case', 'catch', 'class', 'clone', 'const',
        'continue', 'declare', 'default', 'die', 'do', 'echo', 'else', 'elseif', 'empty', 'enddeclare',
        'endfor', 'endforeach', 'endif', 'endswitch', 'endwhile', 'eval', 'exit', 'extends', 'final',
        'finally', 'fn', 'for', 'foreach', 'function', 'global', 'goto', 'if', 'implements', 'include', 'instanceof',
        'insteadof', 'interface', 'isset', 'list', 'match', 'namespace', 'new', 'or', 'print', 'private', 'protected',
        'public', 'require', 'return', 'static', 'switch', 'throw', 'trait', 'try', 'unset', 'use', 'var', 'void',
        'while', 'xor', 'yield',
    ];

    /**
     * @var SerializerInterface
     */
    private $serializer;

    /**
     * @var string
     */
    private $actionInterface;

    /**
     * @var ReflectionClassFactory|null
     */
    private $reflectionClassFactory;

    /**
     * @param CacheInterface $cache
     * @param ModuleReader $moduleReader
     * @param string $actionInterface
     * @param string $cacheKey
     * @param array $reservedWords
     * @param SerializerInterface|null $serializer
     * @param State|null $state
     * @param DirectoryList|null $directoryList
     * @param ReflectionClassFactory|null $reflectionClassFactory
     * @throws FileSystemException
     */
    public function __construct(
        CacheInterface $cache,
        ModuleReader $moduleReader,
        $actionInterface = ActionInterface::class,
        $cacheKey = 'app_action_list',
        $reservedWords = [],
        SerializerInterface $serializer = null,
        State $state = null,
        DirectoryList $directoryList = null,
        ReflectionClassFactory $reflectionClassFactory = null
    ) {
        $this->reservedWords = array_merge($reservedWords, $this->reservedWords);
        $this->actionInterface = $actionInterface;
        $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Serialize::class);
        $state = $state ?: ObjectManager::getInstance()->get(State::class);
        $this->reflectionClassFactory = $reflectionClassFactory
            ?: ObjectManager::getInstance()->get(ReflectionClassFactory::class);

        if ($state->getMode() === State::MODE_PRODUCTION) {
            $directoryList = $directoryList ?: ObjectManager::getInstance()->get(DirectoryList::class);
            $file = $directoryList->getPath(DirectoryList::GENERATED_METADATA)
                . '/' . $cacheKey . '.' . 'php';

            if (file_exists($file)) {
                $this->actions = (include $file) ?? $moduleReader->getActionFiles();
            } else {
                $this->actions = $moduleReader->getActionFiles();
            }
        } else {
            $data = $cache->load($cacheKey);
            if (!$data) {
                $this->actions = $moduleReader->getActionFiles();
                $cache->save($this->serializer->serialize($this->actions), $cacheKey);
            } else {
                $this->actions = $this->serializer->unserialize($data);
            }
        }
    }

    /**
     * Retrieve action class
     *
     * @param string $module
     * @param string $area
     * @param string $namespace
     * @param string $action
     * @return null|string
     * @throws ReflectionException
     */
    public function get($module, $area, $namespace, $action)
    {
        if ($area) {
            $area = '\\' . $area;
        }
        $namespace = $namespace !== null ? strtolower($namespace) : '';
        if (strpos($namespace, self::NOT_ALLOWED_IN_NAMESPACE_PATH) !== false) {
            return null;
        }
        if ($action && in_array(strtolower($action), $this->reservedWords)) {
            $action .= 'action';
        }
        $fullPath = str_replace(
            '_',
            '\\',
            strtolower(
                $module . '\\controller' . $area . '\\' . $namespace . '\\' . $action
            )
        );
        try {
            if ($this->validateActionClass($fullPath)) {
                return $this->actions[$fullPath];
            }
        } catch (ReflectionException $e) {
            return null;
        }

        return null;
    }

    /**
     * Validate Action Class
     *
     * @param string $fullPath
     * @return bool
     * @throws ReflectionException
     */
    private function validateActionClass(string $fullPath): bool
    {
        if (isset($this->actions[$fullPath])) {
            if (!is_subclass_of($this->actions[$fullPath], $this->actionInterface)) {
                return false;
            }
            $reflectionClass = $this->reflectionClassFactory->create($this->actions[$fullPath]);
            if ($reflectionClass->isInstantiable()) {
                return true;
            }
        }
        return false;
    }
}
