<?php

declare(strict_types=1);

namespace Doctrine\ORM\Proxy;

use Closure;
use Doctrine\Common\Proxy\AbstractProxyFactory;
use Doctrine\Common\Proxy\Proxy as CommonProxy;
use Doctrine\Common\Proxy\ProxyDefinition;
use Doctrine\Common\Proxy\ProxyGenerator;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityNotFoundException;
use Doctrine\ORM\Persisters\Entity\EntityPersister;
use Doctrine\ORM\Proxy\Proxy as LegacyProxy;
use Doctrine\ORM\UnitOfWork;
use Doctrine\ORM\Utility\IdentifierFlattener;
use Doctrine\Persistence\Mapping\ClassMetadata;
use ReflectionProperty;
use Symfony\Component\VarExporter\ProxyHelper;
use Symfony\Component\VarExporter\VarExporter;
use Throwable;

use function array_flip;
use function str_replace;
use function strpos;
use function substr;
use function uksort;

/**
 * This factory is used to create proxy objects for entities at runtime.
 */
class ProxyFactory extends AbstractProxyFactory
{
    private const PROXY_CLASS_TEMPLATE = <<<'EOPHP'
<?php

namespace <namespace>;

/**
 * DO NOT EDIT THIS FILE - IT WAS CREATED BY DOCTRINE'S PROXY GENERATOR
 */
class <proxyShortClassName> extends \<className> implements \<baseProxyInterface>
{
    <useLazyGhostTrait>

    public function __construct(?\Closure $initializer = null, ?\Closure $cloner = null)
    {
        if ($cloner !== null) {
            return;
        }

        self::createLazyGhost($initializer, <skippedProperties>, $this);
    }

    public function __isInitialized(): bool
    {
        return isset($this->lazyObjectState) && $this->isLazyObjectInitialized();
    }

    public function __serialize(): array
    {
        <serializeImpl>
    }
}

EOPHP;

    /** @var EntityManagerInterface The EntityManager this factory is bound to. */
    private $em;

    /** @var UnitOfWork The UnitOfWork this factory uses to retrieve persisters */
    private $uow;

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

    /**
     * The IdentifierFlattener used for manipulating identifiers
     *
     * @var IdentifierFlattener
     */
    private $identifierFlattener;

    /** @var ProxyDefinition[] */
    private $definitions = [];

    /**
     * Initializes a new instance of the <tt>ProxyFactory</tt> class that is
     * connected to the given <tt>EntityManager</tt>.
     *
     * @param EntityManagerInterface    $em           The EntityManager the new factory works for.
     * @param string                    $proxyDir     The directory to use for the proxy classes. It must exist.
     * @param string                    $proxyNs      The namespace to use for the proxy classes.
     * @param bool|self::AUTOGENERATE_* $autoGenerate The strategy for automatically generating proxy classes.
     */
    public function __construct(EntityManagerInterface $em, $proxyDir, $proxyNs, $autoGenerate = self::AUTOGENERATE_NEVER)
    {
        $proxyGenerator = new ProxyGenerator($proxyDir, $proxyNs);

        if ($em->getConfiguration()->isLazyGhostObjectEnabled()) {
            $proxyGenerator->setPlaceholder('baseProxyInterface', InternalProxy::class);
            $proxyGenerator->setPlaceholder('useLazyGhostTrait', Closure::fromCallable([$this, 'generateUseLazyGhostTrait']));
            $proxyGenerator->setPlaceholder('skippedProperties', Closure::fromCallable([$this, 'generateSkippedProperties']));
            $proxyGenerator->setPlaceholder('serializeImpl', Closure::fromCallable([$this, 'generateSerializeImpl']));
            $proxyGenerator->setProxyClassTemplate(self::PROXY_CLASS_TEMPLATE);
        } else {
            $proxyGenerator->setPlaceholder('baseProxyInterface', LegacyProxy::class);
        }

        parent::__construct($proxyGenerator, $em->getMetadataFactory(), $autoGenerate);

        $this->em                  = $em;
        $this->uow                 = $em->getUnitOfWork();
        $this->proxyNs             = $proxyNs;
        $this->identifierFlattener = new IdentifierFlattener($this->uow, $em->getMetadataFactory());
    }

    /**
     * {@inheritDoc}
     */
    public function getProxy($className, array $identifier)
    {
        $proxy = parent::getProxy($className, $identifier);

        if (! $this->em->getConfiguration()->isLazyGhostObjectEnabled()) {
            return $proxy;
        }

        $initializer = $this->definitions[$className]->initializer;

        $proxy->__construct(static function (InternalProxy $object) use ($initializer, $proxy): void {
            $initializer($object, $proxy);
        });

        return $proxy;
    }

    /**
     * {@inheritDoc}
     */
    protected function skipClass(ClassMetadata $metadata)
    {
        return $metadata->isMappedSuperclass
            || $metadata->isEmbeddedClass
            || $metadata->getReflectionClass()->isAbstract();
    }

    /**
     * {@inheritDoc}
     */
    protected function createProxyDefinition($className)
    {
        $classMetadata   = $this->em->getClassMetadata($className);
        $entityPersister = $this->uow->getEntityPersister($className);

        if ($this->em->getConfiguration()->isLazyGhostObjectEnabled()) {
            $initializer = $this->createLazyInitializer($classMetadata, $entityPersister);
            $cloner      = static function (): void {
            };
        } else {
            $initializer = $this->createInitializer($classMetadata, $entityPersister);
            $cloner      = $this->createCloner($classMetadata, $entityPersister);
        }

        return $this->definitions[$className] = new ProxyDefinition(
            ClassUtils::generateProxyClassName($className, $this->proxyNs),
            $classMetadata->getIdentifierFieldNames(),
            $classMetadata->getReflectionProperties(),
            $initializer,
            $cloner
        );
    }

    /**
     * Creates a closure capable of initializing a proxy
     *
     * @psalm-return Closure(CommonProxy):void
     *
     * @throws EntityNotFoundException
     */
    private function createInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister): Closure
    {
        $wakeupProxy = $classMetadata->getReflectionClass()->hasMethod('__wakeup');

        return function (CommonProxy $proxy) use ($entityPersister, $classMetadata, $wakeupProxy): void {
            $initializer = $proxy->__getInitializer();
            $cloner      = $proxy->__getCloner();

            $proxy->__setInitializer(null);
            $proxy->__setCloner(null);

            if ($proxy->__isInitialized()) {
                return;
            }

            $properties = $proxy->__getLazyProperties();

            foreach ($properties as $propertyName => $property) {
                if (! isset($proxy->$propertyName)) {
                    $proxy->$propertyName = $properties[$propertyName];
                }
            }

            $proxy->__setInitialized(true);

            if ($wakeupProxy) {
                $proxy->__wakeup();
            }

            $identifier = $classMetadata->getIdentifierValues($proxy);

            try {
                $entity = $entityPersister->loadById($identifier, $proxy);
            } catch (Throwable $exception) {
                $proxy->__setInitializer($initializer);
                $proxy->__setCloner($cloner);
                $proxy->__setInitialized(false);

                throw $exception;
            }

            if ($entity === null) {
                $proxy->__setInitializer($initializer);
                $proxy->__setCloner($cloner);
                $proxy->__setInitialized(false);

                throw EntityNotFoundException::fromClassNameAndIdentifier(
                    $classMetadata->getName(),
                    $this->identifierFlattener->flattenIdentifier($classMetadata, $identifier)
                );
            }
        };
    }

    /**
     * Creates a closure capable of initializing a proxy
     *
     * @return Closure(InternalProxy, InternalProxy):void
     *
     * @throws EntityNotFoundException
     */
    private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister): Closure
    {
        return function (InternalProxy $proxy, InternalProxy $original) use ($entityPersister, $classMetadata): void {
            $identifier = $classMetadata->getIdentifierValues($original);
            $entity     = $entityPersister->loadById($identifier, $original);

            if ($entity === null) {
                throw EntityNotFoundException::fromClassNameAndIdentifier(
                    $classMetadata->getName(),
                    $this->identifierFlattener->flattenIdentifier($classMetadata, $identifier)
                );
            }

            if ($proxy === $original) {
                return;
            }

            $class = $entityPersister->getClassMetadata();

            foreach ($class->getReflectionProperties() as $property) {
                if (! $class->hasField($property->name) && ! $class->hasAssociation($property->name)) {
                    continue;
                }

                $property->setAccessible(true);
                $property->setValue($proxy, $property->getValue($entity));
            }
        };
    }

    /**
     * Creates a closure capable of finalizing state a cloned proxy
     *
     * @psalm-return Closure(CommonProxy):void
     *
     * @throws EntityNotFoundException
     */
    private function createCloner(ClassMetadata $classMetadata, EntityPersister $entityPersister): Closure
    {
        return function (CommonProxy $proxy) use ($entityPersister, $classMetadata): void {
            if ($proxy->__isInitialized()) {
                return;
            }

            $proxy->__setInitialized(true);
            $proxy->__setInitializer(null);

            $class      = $entityPersister->getClassMetadata();
            $identifier = $classMetadata->getIdentifierValues($proxy);
            $original   = $entityPersister->loadById($identifier);

            if ($original === null) {
                throw EntityNotFoundException::fromClassNameAndIdentifier(
                    $classMetadata->getName(),
                    $this->identifierFlattener->flattenIdentifier($classMetadata, $identifier)
                );
            }

            foreach ($class->getReflectionProperties() as $property) {
                if (! $class->hasField($property->name) && ! $class->hasAssociation($property->name)) {
                    continue;
                }

                $property->setAccessible(true);
                $property->setValue($proxy, $property->getValue($original));
            }
        };
    }

    private function generateUseLazyGhostTrait(ClassMetadata $class): string
    {
        $code = ProxyHelper::generateLazyGhost($class->getReflectionClass());
        $code = substr($code, 7 + (int) strpos($code, "\n{"));
        $code = substr($code, 0, (int) strpos($code, "\n}"));
        $code = str_replace('LazyGhostTrait;', str_replace("\n    ", "\n", 'LazyGhostTrait {
            initializeLazyObject as __load;
            setLazyObjectAsInitialized as public __setInitialized;
            isLazyObjectInitialized as private;
            createLazyGhost as private;
            resetLazyObject as private;
        }'), $code);

        return $code;
    }

    private function generateSkippedProperties(ClassMetadata $class): string
    {
        $skippedProperties = [];
        $identifiers       = array_flip($class->getIdentifierFieldNames());
        $filter            = ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PRIVATE;
        $reflector         = $class->getReflectionClass();

        while ($reflector) {
            foreach ($reflector->getProperties($filter) as $property) {
                $name = $property->name;

                if ($property->isStatic() || (($class->hasField($name) || $class->hasAssociation($name)) && ! isset($identifiers[$name]))) {
                    continue;
                }

                $prefix = $property->isPrivate() ? "\0" . $property->class . "\0" : ($property->isProtected() ? "\0*\0" : '');

                $skippedProperties[$prefix . $name] = true;
            }

            $filter    = ReflectionProperty::IS_PRIVATE;
            $reflector = $reflector->getParentClass();
        }

        uksort($skippedProperties, 'strnatcmp');

        $code = VarExporter::export($skippedProperties);
        $code = str_replace(VarExporter::export($class->getName()), 'parent::class', $code);
        $code = str_replace("\n", "\n        ", $code);

        return $code;
    }

    private function generateSerializeImpl(ClassMetadata $class): string
    {
        $reflector  = $class->getReflectionClass();
        $properties = $reflector->hasMethod('__serialize') ? 'parent::__serialize()' : '(array) $this';

        $code = '$properties = ' . $properties . ';
        unset($properties["\0" . self::class . "\0lazyObjectState"], $properties[\'__isCloning\']);

        ';

        if ($reflector->hasMethod('__serialize') || ! $reflector->hasMethod('__sleep')) {
            return $code . 'return $properties;';
        }

        return $code . '$data = [];

        foreach (parent::__sleep() as $name) {
            $value = $properties[$k = $name] ?? $properties[$k = "\0*\0$name"] ?? $properties[$k = "\0' . $reflector->name . '\0$name"] ?? $k = null;

            if (null === $k) {
                trigger_error(sprintf(\'serialize(): "%s" returned as member variable from __sleep() but does not exist\', $name), \E_USER_NOTICE);
            } else {
                $data[$k] = $value;
            }
        }

        return $data;';
    }
}
