<?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.

use Shopware\Plugins\ViisonCommon\Structs;
use Shopware\Plugins\ViisonCommon\Subscribers\Common as CommonSubscriber;

/**
 * The Bootstrap of plugins using ViisonCommon should derive from this class. This class makes sure that the
 * newest installed ViisonCommon version gets loaded.
 *
 * This class needs to be manually loaded in the respective plugin via:
 *
 * if (!class_exists('ViisonCommon_Plugin_Bootstrap')) {
 *     require_once('ViisonCommon/PluginBootstrap.php');
 * }
 *
 * Since we cannot make sure that the newest installed version of this file gets used, no other functionality should
 * be implemented here as far as possible.
 */
class ViisonCommon_Plugin_Bootstrap extends Shopware_Components_Plugin_Bootstrap
{
    /**
     * @var boolean $loadedViisonCommonPlugins
     */
    protected static $loadedViisonCommonPlugins = false;

    /**
     * @var array $loadedViisonCommonPaths
     */
    protected static $loadedViisonCommonPaths;

    /**
     * @var string|null $newestViisonCommonPath
     */
    protected static $newestViisonCommonPath = null;

    /**
     * Subscribes the 'startDispatch' event, that is required to load all ViisonCommon subscribers.
     * Finally imports all snippets of ViisonCommon into the database, since they are not included
     * when the plugin's snippets are imported. Finally clears the proxy caches to prevent common
     * errors during Doctrine model updates, which are caused by stale model proxies.
     *
     * Usage: Call this method once in the plugin's 'update()' method.
     *
     * @param string $version
     * @return array|string
     */
    public function update($version)
    {
        // Note: Previous versions of this method used to subscribe the registerViisonCommonSubscribers() method as an
        // event listener for Enlight_Controller_Front_StartDispatch.
        // If there are plugins which extend from previous versions of this class, they may still register that
        // subscriber.
        // A system will only become fully free from these subscribers once all plugins only ship with a ShopwareCommon
        // version that has at least this version of this class.

        $this->importViisonCommonSnippetsIntoDb();

        // Clear the proxy caches to prevent old model classes from being used during the update
        $this->get('shopware.cache_manager')->clearProxyCache();
    }

    /**
     * Imports all snippets defined in the ViisonCommon module into the database.
     * This is necessary, since these snippets will not reside inside the plugins
     * default snippets folder and thereby will not be availabe to Shopwares
     * default snippet loader.
     *
     * Usage: In most cases you don't have to call this method directly.
     */
    public function importViisonCommonSnippetsIntoDb()
    {
        $this->get('shopware.snippet_database_handler')->loadToDatabase($this->getNewestViisonCommonPath() . 'Snippets/');
    }

    /**
     * Registers the default ViisonCommon namespace with the class loaded, using the files of the newest
     * available ViisonCommon version. The registration is only executed once by using several early returns
     * when calling this method repeatedly. This is necessary, because each plugin that contains ViisonCommon
     * will call 'registerViisonCommonNamespace()' at least one. The early return checks are as follows:
     *
     *   1. If a plugin calls this method for the first time, its own path (that is, the path of its ViisonCommon
     *      library) is added to the static '$loadedViisonCommonPaths' array. Every time the same plugin calls
     *      this method again, it finds its own path in the array and returns.
     *   2. If a plugin calls this method for the first time, but another plugin has called it before, it returns.
     *   3. If the 'Shopware\Plugins\ViisonCommon\Classes\Util\Util' class is already registered with the class
     *      loaded, the namespace has been registered before and this method returns. Event though this check
     *      should not be necessary thanks to the first to checks, we leave it in place to avoid erros, in case
     *      the namespace has been registered elsewhere.
     *
     * That said, the first call to this method is the only call, which will reach the end of the method and
     * hence register the ViisonCommon namespace with the class loaded. It must be noted though, that it does not
     * matter, which plugin makes the first call, and the registered namespace will always be backed by the
     * newest ViisonCommon files available in the shopware file system, since 'getNewestViisonCommonPath()' is used.
     *
     * Usage: Call once at the beginning of the plugin bootstrap's 'afterInit()'.
     */
    public function registerViisonCommonNamespace()
    {
        // 1. Check: Allow every plugin to calls this method only once
        static::$loadedViisonCommonPaths = (static::$loadedViisonCommonPaths) ?: [];
        if (isset(static::$loadedViisonCommonPaths[$this->getPluginViisonCommonPath()])) {
            return;
        }
        static::$loadedViisonCommonPaths[$this->getPluginViisonCommonPath()] = true;

        // Make sure the plugin calling this method is registered for the ViisonCommon path negotiation
        $this->ensureGetNewestPathSubscriber();

        // 2. Check: Allow only the first call to this method to register the namespace
        if (count(static::$loadedViisonCommonPaths) > 1) {
            return;
        }

        // 3. Check: Make sure the namespace is not already registered
        $loader = $this->get('loader');
        if ($loader->getClassPath('Shopware\\Plugins\\ViisonCommon\\Classes\\Util\\Util')) {
            return;
        }

        // Register namespace using the path of the newest ViisonCommon library
        $loader->registerNamespace(
            'Shopware\\Plugins\\ViisonCommon',
            $this->getNewestViisonCommonPath()
        );
    }

    /**
     * A legacy event listener that now only deregisters itself.
     *
     * This is an obsolete event listener that used to be registered into s_core_subscribes by {@see self::update()}.
     * Old installations may still have this event subscription, which will be removed the next time this listener is
     * called.
     */
    public function registerViisonCommonSubscribers(\Enlight_Event_EventArgs $args)
    {
        $pluginId = $this->getId();
        $db = $this->get('db');
        $db->query(
            'DELETE FROM s_core_subscribes
            WHERE
                listener LIKE \'%Viison%::registerViisonCommonSubscribers\'
                AND subscribe = \'Enlight_Controller_Front_StartDispatch\''
        );
    }

    /**
     * First checks the event args for a return value, which corresponds to the path of any available
     * ViisonCommon path. If no return value has been set yet, returns the ViisonCommon path of the
     * plugin, for which this subscriber is called. If a (return) path is already set, the ViisonCommon
     * version at that path as loaded and compared to the version of the ViisonCommon version of the
     * calling plugin. If the plugin's ViisonCommon version is newer than the version on at the returned
     * path, it returns the path of the own ViisonCommon version.
     *
     * Usage: Don't call this method directly!
     *
     * @param \Enlight_Event_EventArgs $arguments
     * @return string
     */
    public function onGetNewestViisonCommonPath(\Enlight_Event_EventArgs $args)
    {
        $path = $args->getReturn();
        // Use own path, if no path has been selected so far
        if (!$path) {
            return $this->getPluginViisonCommonPath();
        }

        // Compare the version at the currently selected path with the version at this plugin's path
        $otherVersion = include($path . 'Version.php');
        $myVersion = include($this->getPluginViisonCommonPath() . 'Version.php');
        if (!$otherVersion || version_compare($myVersion, $otherVersion, '>')) {
            return $this->getPluginViisonCommonPath();
        }

        return $path;
    }

    /**
     * The license check is only defined to make this plugin compatible with all plugins, even if they
     * don't use the license check, since the check is performed in this calls too.
     *
     * @see \Shopware\Plugins\ViisonCommon\PluginBootstrap::registerViisonCommonSubscribers()
     *
     * @param boolean $throwException (optional)
     * @return true
     */
    public function checkLicense($throwException = true)
    {
        return true;
    }

    /**
     * Checks whether the plugin calling this method is already listed in 's_core_plugins' and does not
     * already have a subscriber on 'ViisonCommon_GetNewestPath' registered with the event manager.
     * If no existing subscriber is found, a new subscriber on that event is created and registered.
     *
     * Usage: Don't call this method directly!
     */
    protected function ensureGetNewestPathSubscriber()
    {
        // Check for an entry in s_core_plugins for this plugin. This is necessary, because it is only possible
        // to register an Enlight_Event_Handler_Plugin instance, if the plugin already exists in the databse.
        // It the plugin does not exist yet, it is loaded for the first time, e.g. when opening the plugin manager
        // in the backend. Hence it is not necessary to include it in the ViisonCommon path negotiations.
        // This approach works even if the plugin exceuting this method is the only ViisonCommon plugin in the
        // store, because the path negotiation will default to the ViisonCommon path of the executing plugin,
        // if no path is returned during the path negotiation.
        if (!$this->Plugin()) {
            return;
        }

        // Add the subscriber dynamically, but only if it has not yet been registered for this plugin,
        // to avoid duplicate listeners
        $pluginName = $this->Plugin()->getName();
        $existingListeners = array_filter($this->get('events')->getListeners('ViisonCommon_GetNewestPath'), function ($listener) use ($pluginName) {
            return $listener->Plugin()->getName() === $pluginName;
        });
        if (empty($existingListeners)) {
            $this->get('events')->registerListener(new \Enlight_Event_Handler_Plugin(
                'ViisonCommon_GetNewestPath',
                $this->Collection(),
                $pluginName,
                'onGetNewestViisonCommonPath'
            ));
        }
    }

    /**
     * Checks whether the static $newestViisonCommonPath has already be set and, if it has not,
     * loads all ViisonCommon plugins before a 'ViisonCommon_GetNewestPath' filter event is fired,
     * which returns the path to the newest available ViisonCommon library, which is saved in the
     * static field. Finally the path of the newest available ViisonCommon library is returned.
     *
     * Usage: Don't call this method directly!
     *
     * @return string
     */
    protected function getNewestViisonCommonPath()
    {
        if (!static::$newestViisonCommonPath) {
            $this->loadAllViisonCommonPlugins();
            // Determine the path of the newest, locally available ViisonCommon version
            $newestPath = null;
            $newestPath = $this->get('events')->filter(
                'ViisonCommon_GetNewestPath',
                $newestPath
            );
            static::$newestViisonCommonPath = ($newestPath) ?: $this->getPluginViisonCommonPath();
        }

        return static::$newestViisonCommonPath;
    }

    /**
     * Returns the path of the ViisonCommon library, which is contained in the plugin this method
     * is called on.
     *
     * Usage: In most cases you don't have to call this method directly.
     *
     * @return string
     */
    protected function getPluginViisonCommonPath()
    {
        return $this->Path() . 'ViisonCommon/';
    }

    /**
     * Fetches all plugins from the database and checks their respective directories for a 'ViisonCommon'
     * directory. If that directory is found, the plugin manager is used to 'load' the plugin once, which
     * in return triggers a call to 'afterInit()' on the plugins bootstrap class. By using a static flag,
     * this method ensures that even if calling it repeatedly, the plugins are loaded only once.
     *
     * Usage: Don't call this method directly!
     */
    protected function loadAllViisonCommonPlugins()
    {
        if (static::$loadedViisonCommonPlugins) {
            return;
        }
        static::$loadedViisonCommonPlugins = true;

        // Fetch all plugins
        $plugins = $this->get('db')->fetchAll(
            'SELECT name, namespace, source
            FROM s_core_plugins'
        );

        // Determine plugin root path
        $pluginPathParts = explode('/', $this->Path());
        $pluginRoot = implode('/', array_splice($pluginPathParts, 0, -4));

        foreach ($plugins as $plugin) {
            // Check the plugin path for ViisonCommon
            $pluginPath = realpath($pluginRoot . '/' . $plugin['source'] . '/' . $plugin['namespace'] . '/' . $plugin['name']);
            if (file_exists($pluginPath . '/ViisonCommon')) {
                // Get the plugin from the plugin collection to initialize it
                $this->get('plugins')->get($plugin['namespace'])->get($plugin['name']);
            }
        }
    }
}
