<?php

namespace Doctrine\Common\Proxy;

use BackedEnum;
use Doctrine\Common\Proxy\Exception\InvalidArgumentException;
use Doctrine\Common\Proxy\Exception\UnexpectedValueException;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\Persistence\Mapping\ClassMetadata;
use ReflectionIntersectionType;
use ReflectionMethod;
use ReflectionNamedType;
use ReflectionParameter;
use ReflectionProperty;
use ReflectionType;
use ReflectionUnionType;

use function array_combine;
use function array_diff;
use function array_key_exists;
use function array_map;
use function array_slice;
use function array_unique;
use function assert;
use function bin2hex;
use function call_user_func;
use function chmod;
use function class_exists;
use function dirname;
use function explode;
use function file;
use function file_put_contents;
use function get_class;
use function implode;
use function in_array;
use function interface_exists;
use function is_callable;
use function is_dir;
use function is_scalar;
use function is_string;
use function is_writable;
use function lcfirst;
use function ltrim;
use function method_exists;
use function mkdir;
use function preg_match;
use function preg_match_all;
use function preg_replace;
use function preg_split;
use function random_bytes;
use function rename;
use function rtrim;
use function sprintf;
use function str_replace;
use function strpos;
use function strrev;
use function strtolower;
use function strtr;
use function substr;
use function trim;
use function var_export;

use const DIRECTORY_SEPARATOR;
use const PHP_VERSION_ID;
use const PREG_SPLIT_DELIM_CAPTURE;

/**
 * This factory is used to generate proxy classes.
 * It builds proxies from given parameters, a template and class metadata.
 */
class ProxyGenerator
{
    /**
     * Used to match very simple id methods that don't need
     * to be decorated since the identifier is known.
     */
    public const PATTERN_MATCH_ID_METHOD = <<<'EOT'
((?(DEFINE)
  (?<type>\\?[a-z_\x7f-\xff][\w\x7f-\xff]*(?:\\[a-z_\x7f-\xff][\w\x7f-\xff]*)*)
  (?<intersection_type>(?&type)\s*&\s*(?&type))
  (?<union_type>(?:(?:\(\s*(?&intersection_type)\s*\))|(?&type))(?:\s*\|\s*(?:(?:\(\s*(?&intersection_type)\s*\))|(?&type)))+)
)(?:public\s+)?(?:function\s+%s\s*\(\)\s*)\s*(?::\s*(?:(?&union_type)|(?&intersection_type)|(?:\??(?&type)))\s*)?{\s*return\s*\$this->%s;\s*})i
EOT;

    /**
     * The namespace that contains all proxy classes.
     *
     * @var string
     */
    private $proxyNamespace;

    /**
     * The directory that contains all proxy classes.
     *
     * @var string
     */
    private $proxyDirectory;

    /**
     * Map of callables used to fill in placeholders set in the template.
     *
     * @var string[]|callable[]
     */
    protected $placeholders = [
        'baseProxyInterface'   => Proxy::class,
        'additionalProperties' => '',
    ];

    /**
     * Template used as a blueprint to generate proxies.
     *
     * @var string
     */
    protected $proxyClassTemplate = '<?php

namespace <namespace>;
<enumUseStatements>
/**
 * DO NOT EDIT THIS FILE - IT WAS CREATED BY DOCTRINE\'S PROXY GENERATOR
 */
class <proxyShortClassName> extends \<className> implements \<baseProxyInterface>
{
    /**
     * @var \Closure the callback responsible for loading properties in the proxy object. This callback is called with
     *      three parameters, being respectively the proxy object to be initialized, the method that triggered the
     *      initialization process and an array of ordered parameters that were passed to that method.
     *
     * @see \Doctrine\Common\Proxy\Proxy::__setInitializer
     */
    public $__initializer__;

    /**
     * @var \Closure the callback responsible of loading properties that need to be copied in the cloned object
     *
     * @see \Doctrine\Common\Proxy\Proxy::__setCloner
     */
    public $__cloner__;

    /**
     * @var boolean flag indicating if this object was already initialized
     *
     * @see \Doctrine\Persistence\Proxy::__isInitialized
     */
    public $__isInitialized__ = false;

    /**
     * @var array<string, null> properties to be lazy loaded, indexed by property name
     */
    public static $lazyPropertiesNames = <lazyPropertiesNames>;

    /**
     * @var array<string, mixed> default values of properties to be lazy loaded, with keys being the property names
     *
     * @see \Doctrine\Common\Proxy\Proxy::__getLazyProperties
     */
    public static $lazyPropertiesDefaults = <lazyPropertiesDefaults>;

<additionalProperties>

<constructorImpl>

<magicGet>

<magicSet>

<magicIsset>

<sleepImpl>

<wakeupImpl>

<cloneImpl>

    /**
     * Forces initialization of the proxy
     */
    public function __load(): void
    {
        $this->__initializer__ && $this->__initializer__->__invoke($this, \'__load\', []);
    }

    /**
     * {@inheritDoc}
     * @internal generated method: use only when explicitly handling proxy specific loading logic
     */
    public function __isInitialized(): bool
    {
        return $this->__isInitialized__;
    }

    /**
     * {@inheritDoc}
     * @internal generated method: use only when explicitly handling proxy specific loading logic
     */
    public function __setInitialized($initialized): void
    {
        $this->__isInitialized__ = $initialized;
    }

    /**
     * {@inheritDoc}
     * @internal generated method: use only when explicitly handling proxy specific loading logic
     */
    public function __setInitializer(\Closure $initializer = null): void
    {
        $this->__initializer__ = $initializer;
    }

    /**
     * {@inheritDoc}
     * @internal generated method: use only when explicitly handling proxy specific loading logic
     */
    public function __getInitializer(): ?\Closure
    {
        return $this->__initializer__;
    }

    /**
     * {@inheritDoc}
     * @internal generated method: use only when explicitly handling proxy specific loading logic
     */
    public function __setCloner(\Closure $cloner = null): void
    {
        $this->__cloner__ = $cloner;
    }

    /**
     * {@inheritDoc}
     * @internal generated method: use only when explicitly handling proxy specific cloning logic
     */
    public function __getCloner(): ?\Closure
    {
        return $this->__cloner__;
    }

    /**
     * {@inheritDoc}
     * @internal generated method: use only when explicitly handling proxy specific loading logic
     * @deprecated no longer in use - generated code now relies on internal components rather than generated public API
     * @static
     */
    public function __getLazyProperties(): array
    {
        return self::$lazyPropertiesDefaults;
    }

    <methods>
}
';

    /**
     * Initializes a new instance of the <tt>ProxyFactory</tt> class that is
     * connected to the given <tt>EntityManager</tt>.
     *
     * @param string $proxyDirectory The directory to use for the proxy classes. It must exist.
     * @param string $proxyNamespace The namespace to use for the proxy classes.
     *
     * @throws InvalidArgumentException
     */
    public function __construct($proxyDirectory, $proxyNamespace)
    {
        if (! $proxyDirectory) {
            throw InvalidArgumentException::proxyDirectoryRequired();
        }

        if (! $proxyNamespace) {
            throw InvalidArgumentException::proxyNamespaceRequired();
        }

        $this->proxyDirectory = $proxyDirectory;
        $this->proxyNamespace = $proxyNamespace;
    }

    /**
     * Sets a placeholder to be replaced in the template.
     *
     * @param string          $name
     * @param string|callable $placeholder
     *
     * @throws InvalidArgumentException
     */
    public function setPlaceholder($name, $placeholder)
    {
        if (! is_string($placeholder) && ! is_callable($placeholder)) {
            throw InvalidArgumentException::invalidPlaceholder($name);
        }

        $this->placeholders[$name] = $placeholder;
    }

    /**
     * Sets the base template used to create proxy classes.
     *
     * @param string $proxyClassTemplate
     */
    public function setProxyClassTemplate($proxyClassTemplate)
    {
        $this->proxyClassTemplate = (string) $proxyClassTemplate;
    }

    /**
     * Generates a proxy class file.
     *
     * @param ClassMetadata $class    Metadata for the original class.
     * @param string|bool   $fileName Filename (full path) for the generated class. If none is given, eval() is used.
     *
     * @throws InvalidArgumentException
     * @throws UnexpectedValueException
     */
    public function generateProxyClass(ClassMetadata $class, $fileName = false)
    {
        $this->verifyClassCanBeProxied($class);

        preg_match_all('(<([a-zA-Z]+)>)', $this->proxyClassTemplate, $placeholderMatches);

        $placeholderMatches = array_combine($placeholderMatches[0], $placeholderMatches[1]);
        $placeholders       = [];

        foreach ($placeholderMatches as $placeholder => $name) {
            $placeholders[$placeholder] = $this->placeholders[$name] ?? [$this, 'generate' . $name];
        }

        foreach ($placeholders as & $placeholder) {
            if (! is_callable($placeholder)) {
                continue;
            }

            $placeholder = call_user_func($placeholder, $class);
        }

        $proxyCode = strtr($this->proxyClassTemplate, $placeholders);

        if (! $fileName) {
            $proxyClassName = $this->generateNamespace($class) . '\\' . $this->generateProxyShortClassName($class);

            if (! class_exists($proxyClassName)) {
                eval(substr($proxyCode, 5));
            }

            return;
        }

        $parentDirectory = dirname($fileName);

        if (! is_dir($parentDirectory) && (@mkdir($parentDirectory, 0775, true) === false)) {
            throw UnexpectedValueException::proxyDirectoryNotWritable($this->proxyDirectory);
        }

        if (! is_writable($parentDirectory)) {
            throw UnexpectedValueException::proxyDirectoryNotWritable($this->proxyDirectory);
        }

        $tmpFileName = $fileName . '.' . bin2hex(random_bytes(12));

        file_put_contents($tmpFileName, $proxyCode);
        @chmod($tmpFileName, 0664);
        rename($tmpFileName, $fileName);
    }

    /** @throws InvalidArgumentException */
    private function verifyClassCanBeProxied(ClassMetadata $class)
    {
        if ($class->getReflectionClass()->isFinal()) {
            throw InvalidArgumentException::classMustNotBeFinal($class->getName());
        }

        if ($class->getReflectionClass()->isAbstract()) {
            throw InvalidArgumentException::classMustNotBeAbstract($class->getName());
        }

        if (PHP_VERSION_ID >= 80200 && $class->getReflectionClass()->isReadOnly()) {
            throw InvalidArgumentException::classMustNotBeReadOnly($class->getName());
        }
    }

    /**
     * Generates the proxy short class name to be used in the template.
     *
     * @return string
     */
    private function generateProxyShortClassName(ClassMetadata $class)
    {
        $proxyClassName = ClassUtils::generateProxyClassName($class->getName(), $this->proxyNamespace);
        $parts          = explode('\\', strrev($proxyClassName), 2);

        return strrev($parts[0]);
    }

    /**
     * Generates the proxy namespace.
     *
     * @return string
     */
    private function generateNamespace(ClassMetadata $class)
    {
        $proxyClassName = ClassUtils::generateProxyClassName($class->getName(), $this->proxyNamespace);
        $parts          = explode('\\', strrev($proxyClassName), 2);

        return strrev($parts[1]);
    }

    /**
     * Enums must have a use statement when used as public property defaults.
     */
    public function generateEnumUseStatements(ClassMetadata $class): string
    {
        if (PHP_VERSION_ID < 80100) {
            return "\n";
        }

        $defaultProperties          = $class->getReflectionClass()->getDefaultProperties();
        $lazyLoadedPublicProperties = $this->getLazyLoadedPublicPropertiesNames($class);
        $enumClasses                = [];

        foreach ($class->getReflectionClass()->getProperties(ReflectionProperty::IS_PUBLIC) as $property) {
            $name = $property->getName();

            if (! in_array($name, $lazyLoadedPublicProperties, true)) {
                continue;
            }

            if (array_key_exists($name, $defaultProperties) && $defaultProperties[$name] instanceof BackedEnum) {
                $enumClassNameParts = explode('\\', get_class($defaultProperties[$name]));
                $enumClasses[]      = $enumClassNameParts[0];
            }
        }

        return implode(
            "\n",
            array_map(
                static function ($className) {
                    return 'use ' . $className . ';';
                },
                array_unique($enumClasses)
            )
        ) . "\n";
    }

    /**
     * Generates the original class name.
     *
     * @return string
     */
    private function generateClassName(ClassMetadata $class)
    {
        return ltrim($class->getName(), '\\');
    }

    /**
     * Generates the array representation of lazy loaded public properties and their default values.
     *
     * @return string
     */
    private function generateLazyPropertiesNames(ClassMetadata $class)
    {
        $lazyPublicProperties = $this->getLazyLoadedPublicPropertiesNames($class);
        $values               = [];

        foreach ($lazyPublicProperties as $name) {
            $values[$name] = null;
        }

        return var_export($values, true);
    }

    /**
     * Generates the array representation of lazy loaded public properties names.
     *
     * @return string
     */
    private function generateLazyPropertiesDefaults(ClassMetadata $class)
    {
        return var_export($this->getLazyLoadedPublicProperties($class), true);
    }

    /**
     * Generates the constructor code (un-setting public lazy loaded properties, setting identifier field values).
     *
     * @return string
     */
    private function generateConstructorImpl(ClassMetadata $class)
    {
        $constructorImpl = <<<'EOT'
    public function __construct(?\Closure $initializer = null, ?\Closure $cloner = null)
    {

EOT;

        $toUnset = array_map(static function (string $name): string {
            return '$this->' . $name;
        }, $this->getLazyLoadedPublicPropertiesNames($class));

        return $constructorImpl . ($toUnset === [] ? '' : '        unset(' . implode(', ', $toUnset) . ");\n")
            . <<<'EOT'

        $this->__initializer__ = $initializer;
        $this->__cloner__      = $cloner;
    }
EOT;
    }

    /**
     * Generates the magic getter invoked when lazy loaded public properties are requested.
     *
     * @return string
     */
    private function generateMagicGet(ClassMetadata $class)
    {
        $lazyPublicProperties = $this->getLazyLoadedPublicPropertiesNames($class);
        $reflectionClass      = $class->getReflectionClass();
        $hasParentGet         = false;
        $returnReference      = '';
        $inheritDoc           = '';
        $name                 = '$name';
        $parametersString     = '$name';
        $returnTypeHint       = null;

        if ($reflectionClass->hasMethod('__get')) {
            $hasParentGet     = true;
            $inheritDoc       = '{@inheritDoc}';
            $methodReflection = $reflectionClass->getMethod('__get');

            if ($methodReflection->returnsReference()) {
                $returnReference = '& ';
            }

            $methodParameters = $methodReflection->getParameters();
            $name             = '$' . $methodParameters[0]->getName();

            $parametersString = $this->buildParametersString($methodReflection->getParameters(), ['name']);
            $returnTypeHint   = $this->getMethodReturnType($methodReflection);
        }

        if (empty($lazyPublicProperties) && ! $hasParentGet) {
            return '';
        }

        $magicGet = <<<EOT
    /**
     * $inheritDoc
     * @param string \$name
     */
    public function {$returnReference}__get($parametersString)$returnTypeHint
    {

EOT;

        if (! empty($lazyPublicProperties)) {
            $magicGet .= <<<'EOT'
        if (\array_key_exists($name, self::$lazyPropertiesNames)) {
            $this->__initializer__ && $this->__initializer__->__invoke($this, '__get', [$name]);
EOT;

            if ($returnTypeHint === ': void') {
                $magicGet .= "\n            return;";
            } else {
                $magicGet .= "\n            return \$this->\$name;";
            }

            $magicGet .= <<<'EOT'

        }


EOT;
        }

        if ($hasParentGet) {
            $magicGet .= <<<'EOT'
        $this->__initializer__ && $this->__initializer__->__invoke($this, '__get', [$name]);
EOT;

            if ($returnTypeHint === ': void') {
                $magicGet .= <<<'EOT'

        parent::__get($name);
        return;
EOT;
            } elseif ($returnTypeHint === ': never') {
                $magicGet .= <<<'EOT'

        parent::__get($name);
EOT;
            } else {
                $magicGet .= <<<'EOT'

        return parent::__get($name);
EOT;
            }
        } else {
            $magicGet .= sprintf(<<<EOT
        trigger_error(sprintf('Undefined property: %%s::$%%s', __CLASS__, %s), E_USER_NOTICE);

EOT
                , $name);
        }

        return $magicGet . "\n    }";
    }

    /**
     * Generates the magic setter (currently unused).
     *
     * @return string
     */
    private function generateMagicSet(ClassMetadata $class)
    {
        $lazyPublicProperties = $this->getLazyLoadedPublicPropertiesNames($class);
        $reflectionClass      = $class->getReflectionClass();
        $hasParentSet         = false;
        $inheritDoc           = '';
        $parametersString     = '$name, $value';
        $returnTypeHint       = null;

        if ($reflectionClass->hasMethod('__set')) {
            $hasParentSet     = true;
            $inheritDoc       = '{@inheritDoc}';
            $methodReflection = $reflectionClass->getMethod('__set');

            $parametersString = $this->buildParametersString($methodReflection->getParameters(), ['name', 'value']);
            $returnTypeHint   = $this->getMethodReturnType($methodReflection);
        }

        if (empty($lazyPublicProperties) && ! $hasParentSet) {
            return '';
        }

        $magicSet = <<<EOT
    /**
     * $inheritDoc
     * @param string \$name
     * @param mixed  \$value
     */
    public function __set($parametersString)$returnTypeHint
    {

EOT;

        if (! empty($lazyPublicProperties)) {
            $magicSet .= <<<'EOT'
        if (\array_key_exists($name, self::$lazyPropertiesNames)) {
            $this->__initializer__ && $this->__initializer__->__invoke($this, '__set', [$name, $value]);

            $this->$name = $value;

            return;
        }


EOT;
        }

        if ($hasParentSet) {
            $magicSet .= <<<'EOT'
        $this->__initializer__ && $this->__initializer__->__invoke($this, '__set', [$name, $value]);
EOT;

            if ($returnTypeHint === ': void') {
                $magicSet .= <<<'EOT'

        parent::__set($name, $value);
        return;
EOT;
            } elseif ($returnTypeHint === ': never') {
                $magicSet .= <<<'EOT'

        parent::__set($name, $value);
EOT;
            } else {
                $magicSet .= <<<'EOT'

        return parent::__set($name, $value);
EOT;
            }
        } else {
            $magicSet .= '        $this->$name = $value;';
        }

        return $magicSet . "\n    }";
    }

    /**
     * Generates the magic issetter invoked when lazy loaded public properties are checked against isset().
     *
     * @return string
     */
    private function generateMagicIsset(ClassMetadata $class)
    {
        $lazyPublicProperties = $this->getLazyLoadedPublicPropertiesNames($class);
        $hasParentIsset       = $class->getReflectionClass()->hasMethod('__isset');
        $parametersString     = '$name';
        $returnTypeHint       = null;

        if ($hasParentIsset) {
            $methodReflection = $class->getReflectionClass()->getMethod('__isset');
            $parametersString = $this->buildParametersString($methodReflection->getParameters(), ['name']);
            $returnTypeHint   = $this->getMethodReturnType($methodReflection);
        }

        if (empty($lazyPublicProperties) && ! $hasParentIsset) {
            return '';
        }

        $inheritDoc = $hasParentIsset ? '{@inheritDoc}' : '';
        $magicIsset = <<<EOT
    /**
     * $inheritDoc
     * @param  string \$name
     * @return boolean
     */
    public function __isset($parametersString)$returnTypeHint
    {

EOT;

        if (! empty($lazyPublicProperties)) {
            $magicIsset .= <<<'EOT'
        if (\array_key_exists($name, self::$lazyPropertiesNames)) {
            $this->__initializer__ && $this->__initializer__->__invoke($this, '__isset', [$name]);

            return isset($this->$name);
        }


EOT;
        }

        if ($hasParentIsset) {
            $magicIsset .= <<<'EOT'
        $this->__initializer__ && $this->__initializer__->__invoke($this, '__isset', [$name]);

        return parent::__isset($name);
EOT;
        } else {
            $magicIsset .= '        return false;';
        }

        return $magicIsset . "\n    }";
    }

    /**
     * Generates implementation for the `__sleep` method of proxies.
     *
     * @return string
     */
    private function generateSleepImpl(ClassMetadata $class)
    {
        $reflectionClass = $class->getReflectionClass();

        $hasParentSleep = $reflectionClass->hasMethod('__sleep');
        $inheritDoc     = $hasParentSleep ? '{@inheritDoc}' : '';
        $returnTypeHint = $hasParentSleep ? $this->getMethodReturnType($reflectionClass->getMethod('__sleep')) : '';
        $sleepImpl      = <<<EOT
    /**
     * $inheritDoc
     * @return array
     */
    public function __sleep()$returnTypeHint
    {

EOT;

        if ($hasParentSleep) {
            return $sleepImpl . <<<'EOT'
        $properties = array_merge(['__isInitialized__'], parent::__sleep());

        if ($this->__isInitialized__) {
            $properties = array_diff($properties, array_keys(self::$lazyPropertiesNames));
        }

        return $properties;
    }
EOT;
        }

        $allProperties = ['__isInitialized__'];

        foreach ($class->getReflectionClass()->getProperties() as $prop) {
            assert($prop instanceof ReflectionProperty);
            if ($prop->isStatic()) {
                continue;
            }

            $allProperties[] = $prop->isPrivate()
                ? "\0" . $prop->getDeclaringClass()->getName() . "\0" . $prop->getName()
                : $prop->getName();
        }

        $lazyPublicProperties = $this->getLazyLoadedPublicPropertiesNames($class);
        $protectedProperties  = array_diff($allProperties, $lazyPublicProperties);

        foreach ($allProperties as &$property) {
            $property = var_export($property, true);
        }

        foreach ($protectedProperties as &$property) {
            $property = var_export($property, true);
        }

        $allProperties       = implode(', ', $allProperties);
        $protectedProperties = implode(', ', $protectedProperties);

        return $sleepImpl . <<<EOT
        if (\$this->__isInitialized__) {
            return [$allProperties];
        }

        return [$protectedProperties];
    }
EOT;
    }

    /**
     * Generates implementation for the `__wakeup` method of proxies.
     *
     * @return string
     */
    private function generateWakeupImpl(ClassMetadata $class)
    {
        $reflectionClass = $class->getReflectionClass();

        $hasParentWakeup = $reflectionClass->hasMethod('__wakeup');

        $unsetPublicProperties = [];
        foreach ($this->getLazyLoadedPublicPropertiesNames($class) as $lazyPublicProperty) {
            $unsetPublicProperties[] = '$this->' . $lazyPublicProperty;
        }

        $shortName      = $this->generateProxyShortClassName($class);
        $inheritDoc     = $hasParentWakeup ? '{@inheritDoc}' : '';
        $returnTypeHint = $hasParentWakeup ? $this->getMethodReturnType($reflectionClass->getMethod('__wakeup')) : '';
        $wakeupImpl     = <<<EOT
    /**
     * $inheritDoc
     */
    public function __wakeup()$returnTypeHint
    {
        if ( ! \$this->__isInitialized__) {
            \$this->__initializer__ = function ($shortName \$proxy) {
                \$proxy->__setInitializer(null);
                \$proxy->__setCloner(null);

                \$existingProperties = get_object_vars(\$proxy);

                foreach (\$proxy::\$lazyPropertiesDefaults as \$property => \$defaultValue) {
                    if ( ! array_key_exists(\$property, \$existingProperties)) {
                        \$proxy->\$property = \$defaultValue;
                    }
                }
            };

EOT;

        if (! empty($unsetPublicProperties)) {
            $wakeupImpl .= "\n            unset(" . implode(', ', $unsetPublicProperties) . ');';
        }

        $wakeupImpl .= "\n        }";

        if ($hasParentWakeup) {
            $wakeupImpl .= "\n        parent::__wakeup();";
        }

        $wakeupImpl .= "\n    }";

        return $wakeupImpl;
    }

    /**
     * Generates implementation for the `__clone` method of proxies.
     *
     * @return string
     */
    private function generateCloneImpl(ClassMetadata $class)
    {
        $reflectionClass = $class->getReflectionClass();
        $hasParentClone  = $reflectionClass->hasMethod('__clone');
        $returnTypeHint  = $hasParentClone ? $this->getMethodReturnType($reflectionClass->getMethod('__clone')) : '';
        $inheritDoc      = $hasParentClone ? '{@inheritDoc}' : '';
        $callParentClone = $hasParentClone ? "\n        parent::__clone();\n" : '';

        return <<<EOT
    /**
     * $inheritDoc
     */
    public function __clone()$returnTypeHint
    {
        \$this->__cloner__ && \$this->__cloner__->__invoke(\$this, '__clone', []);
$callParentClone    }
EOT;
    }

    /**
     * Generates decorated methods by picking those available in the parent class.
     *
     * @return string
     */
    private function generateMethods(ClassMetadata $class)
    {
        $methods           = '';
        $methodNames       = [];
        $reflectionMethods = $class->getReflectionClass()->getMethods(ReflectionMethod::IS_PUBLIC);
        $skippedMethods    = [
            '__sleep'   => true,
            '__clone'   => true,
            '__wakeup'  => true,
            '__get'     => true,
            '__set'     => true,
            '__isset'   => true,
        ];

        foreach ($reflectionMethods as $method) {
            $name = $method->getName();

            if (
                $method->isConstructor() ||
                isset($skippedMethods[strtolower($name)]) ||
                isset($methodNames[$name]) ||
                $method->isFinal() ||
                $method->isStatic() ||
                ( ! $method->isPublic())
            ) {
                continue;
            }

            $methodNames[$name] = true;
            $methods           .= "\n    /**\n"
                . "     * {@inheritDoc}\n"
                . "     */\n"
                . '    public function ';

            if ($method->returnsReference()) {
                $methods .= '&';
            }

            $methods .= $name . '(' . $this->buildParametersString($method->getParameters()) . ')';
            $methods .= $this->getMethodReturnType($method);
            $methods .= "\n" . '    {' . "\n";

            if ($this->isShortIdentifierGetter($method, $class)) {
                $identifier = lcfirst(substr($name, 3));
                $fieldType  = $class->getTypeOfField($identifier);
                $cast       = in_array($fieldType, ['integer', 'smallint']) ? '(int) ' : '';

                $methods .= '        if ($this->__isInitialized__ === false) {' . "\n";
                $methods .= '            ';
                $methods .= $this->shouldProxiedMethodReturn($method) ? 'return ' : '';
                $methods .= $cast . ' parent::' . $method->getName() . "();\n";
                $methods .= '        }' . "\n\n";
            }

            $invokeParamsString = implode(', ', $this->getParameterNamesForInvoke($method->getParameters()));
            $callParamsString   = implode(', ', $this->getParameterNamesForParentCall($method->getParameters()));

            $methods .= "\n        \$this->__initializer__ "
                . '&& $this->__initializer__->__invoke($this, ' . var_export($name, true)
                . ', [' . $invokeParamsString . ']);'
                . "\n\n        "
                . ($this->shouldProxiedMethodReturn($method) ? 'return ' : '')
                . 'parent::' . $name . '(' . $callParamsString . ');'
                . "\n" . '    }' . "\n";
        }

        return $methods;
    }

    /**
     * Generates the Proxy file name.
     *
     * @param string $className
     * @param string $baseDirectory Optional base directory for proxy file name generation.
     *                              If not specified, the directory configured on the Configuration of the
     *                              EntityManager will be used by this factory.
     * @psalm-param class-string $className
     *
     * @return string
     */
    public function getProxyFileName($className, $baseDirectory = null)
    {
        $baseDirectory = $baseDirectory ?: $this->proxyDirectory;

        return rtrim($baseDirectory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . Proxy::MARKER
            . str_replace('\\', '', $className) . '.php';
    }

    /**
     * Checks if the method is a short identifier getter.
     *
     * What does this mean? For proxy objects the identifier is already known,
     * however accessing the getter for this identifier usually triggers the
     * lazy loading, leading to a query that may not be necessary if only the
     * ID is interesting for the userland code (for example in views that
     * generate links to the entity, but do not display anything else).
     *
     * @param ReflectionMethod $method
     *
     * @return bool
     */
    private function isShortIdentifierGetter($method, ClassMetadata $class)
    {
        $identifier = lcfirst(substr($method->getName(), 3));
        $startLine  = $method->getStartLine();
        $endLine    = $method->getEndLine();
        $cheapCheck = $method->getNumberOfParameters() === 0
            && substr($method->getName(), 0, 3) === 'get'
            && in_array($identifier, $class->getIdentifier(), true)
            && $class->hasField($identifier)
            && ($endLine - $startLine <= 4);

        if ($cheapCheck) {
            $code = file($method->getFileName());
            $code = trim(implode(' ', array_slice($code, $startLine - 1, $endLine - $startLine + 1)));

            $pattern = sprintf(self::PATTERN_MATCH_ID_METHOD, $method->getName(), $identifier);

            if (preg_match($pattern, $code)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Generates the list of public properties to be lazy loaded.
     *
     * @return array<int, string>
     */
    private function getLazyLoadedPublicPropertiesNames(ClassMetadata $class): array
    {
        $properties = [];

        foreach ($class->getReflectionClass()->getProperties(ReflectionProperty::IS_PUBLIC) as $property) {
            $name = $property->getName();

            if ((! $class->hasField($name) && ! $class->hasAssociation($name)) || $class->isIdentifier($name)) {
                continue;
            }

            $properties[] = $name;
        }

        return $properties;
    }

    /**
     * Generates the list of default values of public properties.
     *
     * @return mixed[]
     */
    private function getLazyLoadedPublicProperties(ClassMetadata $class)
    {
        $defaultProperties          = $class->getReflectionClass()->getDefaultProperties();
        $lazyLoadedPublicProperties = $this->getLazyLoadedPublicPropertiesNames($class);
        $defaultValues              = [];

        foreach ($class->getReflectionClass()->getProperties(ReflectionProperty::IS_PUBLIC) as $property) {
            $name = $property->getName();

            if (! in_array($name, $lazyLoadedPublicProperties, true)) {
                continue;
            }

            if (array_key_exists($name, $defaultProperties)) {
                $defaultValues[$name] = $defaultProperties[$name];
            } elseif (method_exists($property, 'getType')) {
                $propertyType = $property->getType();
                if ($propertyType !== null && $propertyType->allowsNull()) {
                    $defaultValues[$name] = null;
                }
            }
        }

        return $defaultValues;
    }

    /**
     * @param ReflectionParameter[] $parameters
     * @param string[]              $renameParameters
     *
     * @return string
     */
    private function buildParametersString(array $parameters, array $renameParameters = [])
    {
        $parameterDefinitions = [];

        $i = -1;
        foreach ($parameters as $param) {
            assert($param instanceof ReflectionParameter);
            $i++;
            $parameterDefinition = '';
            $parameterType       = $this->getParameterType($param);

            if ($parameterType !== null) {
                $parameterDefinition .= $parameterType . ' ';
            }

            if ($param->isPassedByReference()) {
                $parameterDefinition .= '&';
            }

            if ($param->isVariadic()) {
                $parameterDefinition .= '...';
            }

            $parameterDefinition .= '$' . ($renameParameters ? $renameParameters[$i] : $param->getName());
            $parameterDefinition .= $this->getParameterDefaultValue($param);

            $parameterDefinitions[] = $parameterDefinition;
        }

        return implode(', ', $parameterDefinitions);
    }

    /** @return string|null */
    private function getParameterType(ReflectionParameter $parameter)
    {
        if (! $parameter->hasType()) {
            return null;
        }

        $declaringFunction = $parameter->getDeclaringFunction();

        assert($declaringFunction instanceof ReflectionMethod);

        return $this->formatType($parameter->getType(), $declaringFunction, $parameter);
    }

    /** @return string */
    private function getParameterDefaultValue(ReflectionParameter $parameter)
    {
        if (! $parameter->isDefaultValueAvailable()) {
            return '';
        }

        if (PHP_VERSION_ID < 80100 || is_scalar($parameter->getDefaultValue())) {
            return ' = ' . var_export($parameter->getDefaultValue(), true);
        }

        $value = rtrim(substr(explode('$' . $parameter->getName() . ' = ', (string) $parameter, 2)[1], 0, -2));

        if (strpos($value, '\\') !== false || strpos($value, '::') !== false) {
            $value = preg_split("/('(?:[^'\\\\]*+(?:\\\\.)*+)*+')/", $value, -1, PREG_SPLIT_DELIM_CAPTURE);
            foreach ($value as $i => $part) {
                if ($i % 2 === 0) {
                    $value[$i] = preg_replace('/(?<![a-zA-Z0-9_\x7f-\xff\\\\])[a-zA-Z0-9_\x7f-\xff]++(?:\\\\[a-zA-Z0-9_\x7f-\xff]++|::)++/', '\\\\\0', $part);
                }
            }

            $value = implode('', $value);
        }

        return ' = ' . $value;
    }

    /**
     * @param ReflectionParameter[] $parameters
     *
     * @return string[]
     */
    private function getParameterNamesForInvoke(array $parameters)
    {
        return array_map(
            static function (ReflectionParameter $parameter) {
                return '$' . $parameter->getName();
            },
            $parameters
        );
    }

    /**
     * @param ReflectionParameter[] $parameters
     *
     * @return string[]
     */
    private function getParameterNamesForParentCall(array $parameters)
    {
        return array_map(
            static function (ReflectionParameter $parameter) {
                $name = '';

                if ($parameter->isVariadic()) {
                    $name .= '...';
                }

                $name .= '$' . $parameter->getName();

                return $name;
            },
            $parameters
        );
    }

    /** @return string */
    private function getMethodReturnType(ReflectionMethod $method)
    {
        if (! $method->hasReturnType()) {
            return '';
        }

        return ': ' . $this->formatType($method->getReturnType(), $method);
    }

    /** @return bool */
    private function shouldProxiedMethodReturn(ReflectionMethod $method)
    {
        if (! $method->hasReturnType()) {
            return true;
        }

        return ! in_array(
            strtolower($this->formatType($method->getReturnType(), $method)),
            ['void', 'never'],
            true
        );
    }

    /** @return string */
    private function formatType(
        ReflectionType $type,
        ReflectionMethod $method,
        ?ReflectionParameter $parameter = null
    ) {
        if ($type instanceof ReflectionUnionType) {
            return implode('|', array_map(
                function (ReflectionType $unionedType) use ($method, $parameter) {
                    if ($unionedType instanceof ReflectionIntersectionType) {
                        return '(' . $this->formatType($unionedType, $method, $parameter) . ')';
                    }

                    return $this->formatType($unionedType, $method, $parameter);
                },
                $type->getTypes()
            ));
        }

        if ($type instanceof ReflectionIntersectionType) {
            return implode('&', array_map(
                function (ReflectionType $intersectedType) use ($method, $parameter) {
                    return $this->formatType($intersectedType, $method, $parameter);
                },
                $type->getTypes()
            ));
        }

        assert($type instanceof ReflectionNamedType);

        $name      = $type->getName();
        $nameLower = strtolower($name);

        if ($nameLower === 'static') {
            $name = 'static';
        }

        if ($nameLower === 'self') {
            $name = $method->getDeclaringClass()->getName();
        }

        if ($nameLower === 'parent') {
            $name = $method->getDeclaringClass()->getParentClass()->getName();
        }

        if (! $type->isBuiltin() && ! class_exists($name) && ! interface_exists($name) && $name !== 'static') {
            if ($parameter !== null) {
                throw UnexpectedValueException::invalidParameterTypeHint(
                    $method->getDeclaringClass()->getName(),
                    $method->getName(),
                    $parameter->getName()
                );
            }

            throw UnexpectedValueException::invalidReturnTypeHint(
                $method->getDeclaringClass()->getName(),
                $method->getName()
            );
        }

        if (! $type->isBuiltin() && $name !== 'static') {
            $name = '\\' . $name;
        }

        if (
            $type->allowsNull()
            && ! in_array($name, ['mixed', 'null'], true)
            && ($parameter === null || ! $parameter->isDefaultValueAvailable() || $parameter->getDefaultValue() !== null)
        ) {
            $name = '?' . $name;
        }

        return $name;
    }
}
