<?php
/*
 * Copyright (c) 2023. RTM Business
 */

namespace WordpressModels\ORM;

use Doctrine\Common\EventManager;
use Doctrine\Common\Proxy\AbstractProxyFactory;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\MySQL80Platform;
use Doctrine\DBAL\Schema\MySQLSchemaManager;
use Doctrine\ORM\Cache\DefaultCacheFactory;
use Doctrine\ORM\Cache\RegionsConfiguration;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Exception\MissingMappingDriverImplementation;
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
use Doctrine\ORM\Mapping\Driver\DatabaseDriver;
use Doctrine\ORM\Mapping\UnderscoreNamingStrategy;
use Doctrine\Persistence\Mapping\Driver\MappingDriverChain;
use Doctrine\Persistence\Mapping\MappingException;
use Scienta\DoctrineJsonFunctions\Query\AST\Functions\Mysql as DqlFunctions;
use Symfony\Component\Cache\Adapter\PhpFilesAdapter;
use Symfony\Component\Cache\Exception\CacheException;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Factory + Singleton provider class for Doctrine EntityManager and Configurations.
 */
class EntityManagerFactory
{

    /**
     * @var EntityManagerInterface|null
     * @deprecated 0.5.0 Use Dependency Injection to get the EntityManager. Will be removed in 0.6.0
     */
    private static ?EntityManagerInterface $entityManager = null;


    public function __construct(private ContainerInterface $container)
    {
    }

    /**
     * Create an EntityManager using the WpdbDriver.
     *
     * @throws Exception
     * @throws MissingMappingDriverImplementation
     * @throws MappingException
     * @throws \ReflectionException
     * @deprecated 0.5.0 Use Dependency Injection to get the EntityManager. Will be removed in 0.6.0
     */
    public static function instance(?Configuration $config = null): EntityManagerInterface
    {
        $container = wpm_container();
        if ($container->has(EntityManagerInterface::class)) {
            return $container->get(EntityManagerInterface::class);
        }

        if (self::$entityManager?->isOpen()) {
            return self::$entityManager;
        }

        return self::$entityManager = self::createUnique($config);
    }

    /**
     * Create a Doctrine EntityManager Configuration.
     *
     * Sets the proxy cache directory at WP_CONTENT_DIR/cache/doctrine/proxy
     *
     * Initially, looks for entities in the ./Entity subdirectory of the current directory. Entity locations
     * can be registered using the `doctrine_entity_directories` hook.
     *
     * @throws CacheException
     */
    public function createConfiguration(string $configName = 'doctrine'): Configuration
    {
        $config = new Configuration();
        $config->setNamingStrategy(new UnderscoreNamingStrategy());

        // set the proxy dir per version
        $cacheRootDir = WP_CONTENT_DIR . "/cache/$configName/";
        $config->setProxyDir("$cacheRootDir/proxy");
        $config->setProxyNamespace('DatabaseProxy');

        // generate proxies if the file does not exist or has changed
        $config->setAutoGenerateProxyClasses(AbstractProxyFactory::AUTOGENERATE_FILE_NOT_EXISTS_OR_CHANGED);

        // use the Model directory for entity metadata
        $paths = apply_filters('doctrine_entity_directories', [
            __DIR__ . '/Entity',
        ]);

        $driverChain = new MappingDriverChain();
        $driverChain->setDefaultDriver(new AttributeDriver($paths));

        $config->setMetadataDriverImpl($driverChain);

        self::configSecondLevelCache($config, $cacheRootDir);

        // register json functions
        self::configJsonFunctions($config);

        $config->setEntityListenerResolver($this->container->get('doctrine.orm.default_entity_listener_resolver'));

        return $config;
    }

    /**
     * @param Configuration|null $config
     * @return EntityManagerInterface
     * @throws CacheException
     * @throws Exception
     * @throws MissingMappingDriverImplementation
     */
    public static function createUnique(?Configuration $config = null): EntityManagerInterface
    {
        /** @var EntityManagerFactory $service */
        $service = wpm_container()->get(self::class);
        return $service->create($config);
    }

    /**
     * Create a unique EntityManager using the WpdbDriver.
     *
     * @param Configuration|null $config
     * @return EntityManagerInterface
     * @throws CacheException
     * @throws Exception
     * @throws MissingMappingDriverImplementation
     */
    public function create(?Configuration $config = null): EntityManagerInterface
    {
        $config ??= $this->createConfiguration();
        $eventManager = new EventManager();

        $entityManager = new EntityManager(
            DriverManager::getConnection([
                'driverClass' => !defined('DB_ENGINE')
                    ? WpdbDriver::class
                    : match (DB_ENGINE) {
                        'mysql' => WpdbDriver::class,
                        'mysqli' => WpdbDriver::class,
                        'sqlite' => WpdbSQLiteDriver::class,
                        default => WpdbDriver::class
                    }
            ], $config),
            $config,
            $eventManager
        );

        // register the table prefix injector
        $eventManager->addEventSubscriber(new TablePrefixEventSubscriber());

        // register wordpress hook bridge for doctrine ORM events
        $eventManager->addEventSubscriber(new DoctrineEventHookBridge());

        // register post type discriminators
        $eventManager->addEventSubscriber(new PostTypeClassMetadataListener());

        // add woocommerce post types if installed
        if (function_exists('WC')) {
            WooCommerceEntities::instance();
        }

        // add timestamp listener
        $eventManager->addEventSubscriber(new TimestampListener());


        return $entityManager;
    }

    /**
     * @param Configuration $config
     * @param string $cacheRootDir
     * @return void
     * @throws CacheException
     */
    public static function configSecondLevelCache(Configuration $config, string $cacheRootDir): void
    {
        $secondLevelCacheEnabled = apply_filters('wpm_doctrine_second_level_cache_enabled', true);

        if ($secondLevelCacheEnabled) {
            $config->setSecondLevelCacheEnabled($secondLevelCacheEnabled);

            // create a file adapter for the query cache, with indefinite lifetime
            $queryCache = apply_filters('doctrine_query_adapter', new PhpFilesAdapter('doctrine_query', 0, $cacheRootDir));
            $config->setQueryCache($queryCache);

            // create a file adapter for the result cache, with indefinite lifetime
            $resultCache = apply_filters('doctrine_result_adapter', new PhpFilesAdapter('doctrine_result', 0, $cacheRootDir));
            $config->setResultCache($resultCache);

            // create a file adapter for the metadata cache, with indefinite lifetime
            $metadataCache = apply_filters('doctrine_metadata_adapter', new PhpFilesAdapter('doctrine_metadata', 0, $cacheRootDir));
            $config->setMetadataCache($metadataCache);

            $regionsConfig = new RegionsConfiguration();

            do_action('wpm_doctrine_cache_regions_configuration', $regionsConfig);

            // create the cache pool for the second level cache
            $secondLevelCachePool = apply_filters('doctrine_second_level_cache_pool', new PhpFilesAdapter('doctrine_sl', 0, $cacheRootDir));
            $secondLevelCacheFactory = new DefaultCacheFactory($regionsConfig, $secondLevelCachePool);

            $cacheConfiguration = $config->getSecondLevelCacheConfiguration();
            $cacheConfiguration->setCacheFactory($secondLevelCacheFactory);

            do_action('wpm_doctrine_cache_configuration', $cacheConfiguration);
        }
    }

    private static function configJsonFunctions(Configuration $config)
    {
        $config->addCustomStringFunction(DqlFunctions\JsonExtract::FUNCTION_NAME, DqlFunctions\JsonExtract::class);
        $config->addCustomStringFunction(DqlFunctions\JsonSearch::FUNCTION_NAME, DqlFunctions\JsonSearch::class);
        $config->addCustomStringFunction(DqlFunctions\JsonContains::FUNCTION_NAME, DqlFunctions\JsonContains::class);
        $config->addCustomStringFunction(DqlFunctions\JsonContainsPath::FUNCTION_NAME, DqlFunctions\JsonContainsPath::class);
        $config->addCustomStringFunction(DqlFunctions\JsonDepth::FUNCTION_NAME, DqlFunctions\JsonDepth::class);
        $config->addCustomStringFunction(DqlFunctions\JsonKeys::FUNCTION_NAME, DqlFunctions\JsonKeys::class);
        $config->addCustomStringFunction(DqlFunctions\JsonLength::FUNCTION_NAME, DqlFunctions\JsonLength::class);
        $config->addCustomStringFunction(DqlFunctions\JsonType::FUNCTION_NAME, DqlFunctions\JsonType::class);
    }

    /**
     * Get an enhanced schema manager that can handle the enum type.
     *
     * @param EntityManagerInterface $entityManager
     * @return DatabaseDriver
     * @throws \Doctrine\DBAL\Exception
     */
    public static function getEnhancedSchemaManager(EntityManagerInterface $entityManager): MySQLSchemaManager
    {
        $platform = new MySQL80Platform();
        $platform->registerDoctrineTypeMapping('enum', 'string');

        return new MySQLSchemaManager($entityManager->getConnection(), $platform);
    }

}
