<?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\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\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;

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

    private static ?EntityManagerInterface $entityManager = null;

    /**
     * Create an EntityManager using the WpdbDriver.
     *
     * @throws Exception
     * @throws MissingMappingDriverImplementation
     * @throws MappingException
     * @throws \ReflectionException
     */
    public static function instance(?Configuration $config = null): EntityManagerInterface
    {
        if (self::$entityManager?->isOpen()) {
            return self::$entityManager;
        }

        return self::createUnique($config);
    }

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

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

        // always generate proxies in development mode
        $config->setAutoGenerateProxyClasses(AbstractProxyFactory::AUTOGENERATE_FILE_NOT_EXISTS);

        // 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);

        return $config;
    }

    /**
     * Create a unique EntityManager using the WpdbDriver.
     *
     * @throws Exception
     * @throws MissingMappingDriverImplementation
     */
    public static function createUnique(?Configuration $config = null, string $version = ''): EntityManagerInterface
    {
        if (!$version) {
            // create a version from total plugin directory and plugin versions
            $plugins = wp_get_active_and_valid_plugins();
            sort($plugins);

            $pluginVersions = array_reduce($plugins,
                fn(string $carry, string $plugin) => $plugin . '=' . get_plugin_data($plugin)['Version'], '');
            $theme = wp_get_theme();
            $version = md5("PLUGINS::{$pluginVersions}&THEME::{$theme->get('Name')}={$theme->get('Version')}");
        }
        $config ??= self::createConfiguration($version);
        $eventManager = new EventManager();

        self::$entityManager = new EntityManager(
            DriverManager::getConnection(['driverClass' => 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
        foreach (wp_get_active_and_valid_plugins() as $plugin) {
            // subtract the WP_CONTENT_DIR to get the relative path
            $plugin = str_replace(WP_CONTENT_DIR . 'plugins/', '', $plugin);
            if (strpos($plugin, 'woocommerce/woocommerce.php') !== false) {
                WooCommerceEntities::instance();
                break;
            }
        }


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

        return self::$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);
    }

}
