<?php

/*
 * This file is part of the phpstan-magento package.
 *
 * (c) bitExpert AG
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
declare(strict_types=1);

namespace bitExpert\PHPStan\Magento\Autoload;

use bitExpert\PHPStan\Magento\Autoload\DataProvider\ClassLoaderProvider;
use bitExpert\PHPStan\Magento\Autoload\DataProvider\ExtensionAttributeDataProvider;
use Laminas\Code\Generator\DocBlock\Tag\ParamTag;
use Laminas\Code\Generator\DocBlock\Tag\ReturnTag;
use Laminas\Code\Generator\DocBlockGenerator;
use Laminas\Code\Generator\InterfaceGenerator;
use Laminas\Code\Generator\MethodGenerator;
use Laminas\Code\Generator\ParameterGenerator;
use PHPStan\Cache\Cache;

class ExtensionInterfaceAutoloader implements Autoloader
{
    /**
     * @var Cache
     */
    private $cache;
    /**
     * @var ClassLoaderProvider
     */
    private $classLoaderProvider;
    /**
     * @var ExtensionAttributeDataProvider
     */
    private $attributeDataProvider;

    /**
     * ExtensionInterfaceAutoloader constructor.
     *
     * @param Cache $cache
     * @param ExtensionAttributeDataProvider $attributeDataProvider
     * @param ClassLoaderProvider $classLoaderProvider
     */
    public function __construct(
        Cache $cache,
        ClassLoaderProvider $classLoaderProvider,
        ExtensionAttributeDataProvider $attributeDataProvider
    ) {
        $this->cache = $cache;
        $this->classLoaderProvider = $classLoaderProvider;
        $this->attributeDataProvider = $attributeDataProvider;
    }

    public function autoload(string $interfaceName): void
    {
        if (preg_match('#ExtensionInterface$#', $interfaceName) !== 1) {
            return;
        }

        // fix for PHPStan 1.7.5 and later: Classes generated by autoloaders are supposed to "win" against
        // local classes in your project. We need to check first if classes exists locally before generating them!
        $pathToLocalInterface = $this->classLoaderProvider->findFile($interfaceName);
        if ($pathToLocalInterface === false) {
            $pathToLocalInterface = $this->cache->load($interfaceName, '');
            if ($pathToLocalInterface === null) {
                $this->cache->save($interfaceName, '', $this->getFileContents($interfaceName));
                $pathToLocalInterface = $this->cache->load($interfaceName, '');
            }
        }

        require_once($pathToLocalInterface);
    }

    /**
     * Given an extension attributes interface name, generate that interface (if possible)
     */
    public function getFileContents(string $interfaceName): string
    {
        /**
         * Given a classname to autoload (such as Magento\Catalog\Api\Data\ProductExtensionInterface),
         * generate the entity's interface name (like Magento\Catalog\Api\Data\ProductInterface)
         *
         * @see \Magento\Framework\Code\Generator::generateClass
         * @see \Magento\Framework\Api\Code\Generator\ExtensionAttributesGenerator::__construct
         */
        $sourceInterface = rtrim(substr($interfaceName, 0, -1 * strlen('ExtensionInterface')), '\\') . 'Interface';

        // Magento only creates extension attribute interfaces for existing interfaces; retain that logic
        if (!$this->classLoaderProvider->exists($sourceInterface)) {
            throw new \InvalidArgumentException(
                sprintf('%s does not exist and has no extension interface', $sourceInterface)
            );
        }

        $generator = new InterfaceGenerator();
        $generator
            ->setName($interfaceName)
            ->setImplementedInterfaces(['\Magento\Framework\Api\ExtensionAttributesInterface']);

        $attrs = $this->attributeDataProvider->getAttributesForInterface($sourceInterface);
        foreach ($attrs as $propertyName => $type) {
            /**
             * Generate getters and setters for each extension attribute
             *
             * @see \Magento\Framework\Api\Code\Generator\ExtensionAttributesGenerator::_getClassMethods
             */

            $generator->addMethodFromGenerator(
                MethodGenerator::fromArray([
                    'name' => 'get' . ucfirst($propertyName),
                    'docblock' => DocBlockGenerator::fromArray([
                        'tags' => [
                            new ReturnTag([$type, 'null']),
                        ],
                    ]),
                ])
            );
            $generator->addMethodFromGenerator(
                MethodGenerator::fromArray([
                    'name' => 'set' . ucfirst($propertyName),
                    'parameters' => [$propertyName],
                    'docblock' => DocBlockGenerator::fromArray([
                        'tags' => [
                            new ParamTag($propertyName, [$type]),
                            new ReturnTag('$this')
                        ]
                    ])
                ])
            );
        }

        return "<?php\n\n" . $generator->generate();
    }

    public function register(): void
    {
        \spl_autoload_register([$this, 'autoload'], true, false);
    }

    public function unregister(): void
    {
        \spl_autoload_unregister([$this, 'autoload']);
    }
}
