<?php
// Copyright (c) Pickware GmbH. All rights reserved.
// This file is part of software that is released under a proprietary license.
// You must not copy, modify, distribute, make publicly available, or execute
// its contents or parts thereof without express permission by the copyright
// holder, unless otherwise permitted by law.

namespace VIISON\ShopwarePluginDependencyLoader;

use DirectoryIterator;

final class Loader
{
    /**
     * @var string[]|null $pluginPaths
     */
    private $pluginPaths = null;

    /**
     * @var string[] $loadedDependencies
     */
    private $loadedDependencies;

    /**
     * Returns the singleton instance of this loader class.
     *
     * @return Loader
     */
    public static function getInstance()
    {
        static $instance = null;
        if (!$instance) {
            $instance = new Loader();
        }

        return $instance;
    }

    /**
     * First adds the given $pluginPath to the list of available plugin paths. This is especially
     * important in case a plugin requires its dependencies for the very first time, meaning that
     * the plugin is not even listed in s_core_plugins and hence might not yet be contained in
     * the list. Finally the given list of $dependencies is loaded one by one.
     *
     * @param string $pluginPath
     * @param string[] $dependencies
     */
    public function requireDependencies($pluginPath, array $dependencies)
    {
        $this->pluginPaths[] = realpath($pluginPath);
        foreach ($dependencies as $dependency) {
            $this->loadDependency($dependency);
        }
    }

    /**
     * If the $dependency is not already loaded, all available plugins are checked for an
     * instance of the dependency and the path to the newest version is determined. Finally
     * the dependency's namespace is registered with the Enlight class loader using the
     * path to its newest available version. If the dependency contains custom models in a
     * 'Models' directory, that directory is registered in Shopware's special namespace
     * 'Shopware\CustomModels' as well as with the Doctrine annotation handler.
     *
     * @param string $dependency
     * @throws \Exception If the $dependency cannot be found in any of the available plugins.
     */
    private function loadDependency($dependency)
    {
        // Make sure to load the dependency only once
        if (isset($this->loadedDependencies[$dependency])) {
            return;
        }

        // Determine the path to the newest available version of the dependency
        $newestVersion = '0.0.0';
        $newestVersionPath = null;
        foreach ($this->pluginPaths as $pluginPath) {
            // Check plugin for the dependency
            $realpath = realpath($pluginPath . '/' . $dependency);
            if ($realpath === false) {
                continue;
            }
            $dependencyPath = $realpath . '/';
            if (!file_exists($dependencyPath . 'Version.php')) {
                continue;
            }

            // Check for a newer version
            $availableVersion = include($dependencyPath . 'Version.php');
            if (version_compare($availableVersion, $newestVersion, '>')) {
                $newestVersion = $availableVersion;
                $newestVersionPath = $dependencyPath;
            }
        }
        if (!$newestVersionPath) {
            throw new \Exception(
                sprintf('The dependency "%s" is not available in any plugin.', $dependency)
            );
        }

        // Register the dependency namespace by prepending the newest path to overrule
        // any paths previously registered for the same namespace
        $dependencyNamespace = 'Shopware\\Plugins\\' . $dependency;
        Shopware()->Container()->get('loader')->registerNamespace(
            $dependencyNamespace,
            $newestVersionPath,
            \Enlight_Loader::DEFAULT_SEPARATOR,
            \Enlight_Loader::DEFAULT_EXTENSION,
            \Enlight_Loader::POSITION_PREPEND
        );
        if (file_exists($newestVersionPath . 'Models/')) {
            // Register the models and annotations in the special namespace
            Shopware()->Container()->get('loader')->registerNamespace(
                'Shopware\\CustomModels',
                $newestVersionPath . 'Models/'
            );
            Shopware()->Container()->get('shopware.model_annotations')->addPaths([
                $newestVersionPath . 'Models/'
            ]);
        }
        $this->loadedDependencies[$dependency] = true;
    }

    /**
     * Make private to enforce usage of singleton instance.
     */
    private function __construct()
    {
        $this->loadedDependencies = [];
        $this->pluginPaths = [];

        // Save root paths of all available plugins
        if (Shopware()->Container()->hasParameter('shopware.plugin_directories')) {
            // Shopware >= 5.2
            $pluginRootPaths = Shopware()->Container()->getParameter('shopware.plugin_directories');
            if (!isset($pluginRootPaths['ShopwarePlugins'])) {
                // Shopware < 5.2.19
                $pluginRootPaths['ShopwarePlugins'] = Shopware()->Container()->getParameter('kernel.root_dir') . '/custom/plugins';
            }
        } else {
            // Use fallback paths
            $pluginsPath = Shopware()->Container()->getParameter('kernel.root_dir') . '/engine/Shopware/Plugins/';
            $pluginRootPaths = [
                'Community' => $pluginsPath . 'Community',
                'Default' => $pluginsPath . 'Default',
                'Local' => $pluginsPath . 'Local',
            ];
        }

        $legacyPluginNamespaces = [
            'Backend',
            'Core',
            'Frontend',
        ];

        foreach ($pluginRootPaths as $pathName => $pluginRootPath) {
            if ($pathName === 'ShopwarePlugins' || $pathName === 'ProjectPlugins') {
                // Plugins in the /custom directory for SW 5.2+
                foreach (new DirectoryIterator($pluginRootPath) as $pluginDir) {
                    if ($pluginDir->isDir() && !$pluginDir->isDot()) {
                        $this->pluginPaths[] = $pluginDir->getPathname();
                    }
                }
            } else {
                // Old plugin directories for SW < 5.2
                foreach ($legacyPluginNamespaces as $namespace) {
                    $namespaceDir = $pluginRootPath . '/' . $namespace;
                    if (!file_exists($namespaceDir)) {
                        continue;
                    }

                    foreach (new DirectoryIterator($namespaceDir) as $pluginDir) {
                        if ($pluginDir->isDir() && !$pluginDir->isDot()) {
                            $this->pluginPaths[] = $pluginDir->getPathname();
                        }
                    }
                }
            }
        }
    }

    /**
     * Make private to enforce usage of singleton instance.
     */
    private function __clone()
    {
        //
    }
}
