<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\JsonStreamer\Read;

use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\JsonStreamer\Exception\InvalidArgumentException;
use Symfony\Component\JsonStreamer\Exception\RuntimeException;
use Symfony\Component\VarExporter\LazyGhostTrait;
use Symfony\Component\VarExporter\ProxyHelper;

/**
 * Instantiates a new $className lazy ghost {@see LazyGhostTrait}.
 *
 * Prior to PHP 8.4, the "$className" argument class must not be final.
 * The $initializer must be a callable that sets the actual object values when being called.
 *
 * @author Mathias Arlaud <mathias.arlaud@gmail.com>
 *
 * @internal
 */
final class LazyInstantiator
{
    private ?Filesystem $fs = null;

    /**
     * @var array{reflection: array<class-string, \ReflectionClass<object>>, lazy_class_name: array<class-string, class-string>}
     */
    private static array $cache = [
        'reflection' => [],
        'lazy_class_name' => [],
    ];

    /**
     * @var array<class-string, true>
     */
    private static array $lazyClassesLoaded = [];

    public function __construct(
        private ?string $lazyGhostsDir = null,
    ) {
    }

    /**
     * @template T of object
     *
     * @param class-string<T>   $className
     * @param callable(T): void $initializer
     *
     * @return T
     */
    public function instantiate(string $className, callable $initializer): object
    {
        try {
            $classReflection = self::$cache['reflection'][$className] ??= new \ReflectionClass($className);
        } catch (\ReflectionException $e) {
            throw new RuntimeException($e->getMessage(), $e->getCode(), $e);
        }

        if ($classReflection->isInternal()) {
            $instance = $classReflection->newInstanceWithoutConstructor();
            $initializer($instance);

            return $instance;
        }

        // use native lazy ghosts if available
        if (\PHP_VERSION_ID >= 80400) {
            return $classReflection->newLazyGhost($initializer);
        }

        $this->fs ??= new Filesystem();

        if (null === $this->lazyGhostsDir) {
            throw new InvalidArgumentException('The "$lazyGhostsDir" argument cannot be null when using PHP < 8.4.');
        }

        $lazyClassName = self::$cache['lazy_class_name'][$className] ??= \sprintf('%sGhost', preg_replace('/\\\\/', '', $className));

        if (isset(self::$lazyClassesLoaded[$className]) && class_exists($lazyClassName)) {
            return $lazyClassName::createLazyGhost($initializer);
        }

        if (!is_file($path = \sprintf('%s%s%s.php', $this->lazyGhostsDir, \DIRECTORY_SEPARATOR, hash('xxh128', $className)))) {
            if (!$this->fs->exists($this->lazyGhostsDir)) {
                $this->fs->mkdir($this->lazyGhostsDir);
            }

            $this->fs->dumpFile($path, \sprintf('<?php class %s%s', $lazyClassName, ProxyHelper::generateLazyGhost($classReflection)));
        }

        require_once $path;

        self::$lazyClassesLoaded[$className] = true;

        return $lazyClassName::createLazyGhost($initializer);
    }
}
