<?php
    /**
     * Factory for limesurvey plugin objects.
     */
    class PluginManager {

        /**
         * Object containing any API that the plugins can use.
         * @var object
         */
        protected $api = null;
        /**
         * Array mapping guids to question object class names.
         * @var type 
         */
        protected $guidToQuestion = array();
        
        protected $plugins = array();
        
        protected $pluginDirs = array();
        
        protected $stores = array();

        protected $subscriptions = array();
        
        /**
         * Creates the plugin manager.
         * 
         * @param mixed $api The class name of the API class to load, or
         * a reference to an already constructed reference.
         */
        public function __construct($api) 
        {
            if (is_object($api))
            {
                $this->api = $api;
            }
            else 
            {
                $this->api = new $api();
            }
            
            $this->pluginDirs[] = 'webroot.plugins';           // User plugins
            $this->pluginDirs[] = 'application.core.plugins';  // Core plugins
        }
        
        /**
         * Return a list of installed plugins, but only if the files are still there
         * 
         * This prevents errors when a plugin was installed but the files were removed
         * from the server.
         * 
         * @return array
         */
        public function getInstalledPlugins()
        {
            $pluginModel = Plugin::model();    
            $records = $pluginModel->findAll();
            
            $plugins = array();

            foreach ($records as $record) {
                // Only add plugins we can find
                if ($this->loadPlugin($record->name) !== false) {
                    $plugins[$record->id] = $record;
                }
            }
            return $plugins;
        }
        
        /**
         * Returns the storage instance of type $storageClass.
         * If needed initializes the storage object.
         * @param string $storageClass
         */
        public function getStore($storageClass)
        {
            if (!isset($this->stores[$storageClass]))
            {
                $this->stores[$storageClass] = new $storageClass();
            }
            return $this->stores[$storageClass];
        }

        
        /**
         * This function returns an API object, exposing an API to each plugin.
         * In the current case this is the LimeSurvey API.
         * @return LimesurveyApi
         */
        public function getAPI()
        {
            return $this->api;
        }
        /**
         * Registers a plugin to be notified on some event.
         * @param iPlugin $plugin Reference to the plugin.
         * @param string $event Name of the event.
         * @param string $function Optional function of the plugin to be called.
         */
        public function subscribe(iPlugin $plugin, $event, $function = null)
        {
            if (!isset($this->subscriptions[$event]))
            {
                $this->subscriptions[$event] = array();
            }
            if (!$function)
            {
                $function = $event;
            }
            $subscription = array($plugin, $function);
            // Subscribe only if not yet subscribed.
            if (!in_array($subscription, $this->subscriptions[$event]))
            {
                $this->subscriptions[$event][] = $subscription;
            }


        }

        /**
         * Unsubscribes a plugin from an event.
         * @param iPlugin $plugin Reference to the plugin being unsubscribed.
         * @param string $event Name of the event. Use '*', to unsubscribe all events for the plugin.
         * @param string $function Optional function of the plugin that was registered.
         */
        public function unsubscribe(iPlugin $plugin, $event)
        {
            // Unsubscribe recursively.
            if ($event == '*')
            {
                foreach ($this->subscriptions as $event)
                {
                    $this->unsubscribe($plugin, $event);
                }
            }
            elseif (isset($this->subscriptions[$event]))
            {
                foreach ($this->subscriptions[$event] as $index => $subscription)
                {
                    if ($subscription[0] == $plugin)
                    {
                        unset($this->subscriptions[$event][$index]);
                    }
                }
            }
        }


        /**
         * This function dispatches an event to all registered plugins.
         * @param PluginEvent $event Object holding all event properties
         * @param string|array $target Optional name of plugin to fire the event on
         * 
         * @return PluginEvent
         */
        public function dispatchEvent(PluginEvent $event, $target = array())
        {
            $eventName = $event->getEventName();
            if (is_string($target)) {
                $target = array($target);
            }
            if (isset($this->subscriptions[$eventName]))
            {
                foreach($this->subscriptions[$eventName] as $subscription)
                {
                    if (!$event->isStopped() 
                     && (empty($target) || in_array(get_class($subscription[0]), $target))) 
                    {
                        $subscription[0]->setEvent($event);
                        call_user_func($subscription);
                    }
                }
            }
            
            return $event;
        }

        /**
         * Scans the plugin directory for plugins.
         * This function is not efficient so should only be used in the admin interface
         * that specifically deals with enabling / disabling plugins.
         */
        public function scanPlugins()
        {
            $result = array();
            foreach ($this->pluginDirs as $pluginDir) {
                $currentDir = Yii::getPathOfAlias($pluginDir);
                if (is_dir($currentDir)) {
                    foreach (new DirectoryIterator($currentDir) as $fileInfo)
                    {
                        if (!$fileInfo->isDot() && $fileInfo->isDir())
                        {
                            // Check if the base plugin file exists.
                            // Directory name Example most contain file ExamplePlugin.php.
                            $pluginName = $fileInfo->getFilename();
                            $file = Yii::getPathOfAlias($pluginDir . ".$pluginName.{$pluginName}") . ".php";
                            if (file_exists($file))
                            {
                                $result[$pluginName] = $this->getPluginInfo($pluginName, $pluginDir);
                            }
                        }

                    }
                }
            }

            return $result;
        }

        /**
         * Gets the description of a plugin. The description is accessed via a
         * static function inside the plugin file.
         *
         * @param string $pluginClass The classname of the plugin
         */
        public function getPluginInfo($pluginClass, $pluginDir = null)
        {
            $result = array();
            $class = "{$pluginClass}";
            
            if (!class_exists($class, false)) {
                $found = false;
                if (!is_null($pluginDir)) {
                    $dirs = array($pluginDir);
                } else {
                    $dirs = $this->pluginDirs;
                }
                
                foreach ($this->pluginDirs as $pluginDir) {
                    $file = Yii::getPathOfAlias($pluginDir . ".$pluginClass.{$pluginClass}") . ".php";
                    if (file_exists($file)) {
                        Yii::import($pluginDir . ".$pluginClass.*");
                        $found = true;
                        break;
                    }
                }
                
                if (!$found) {
                    return false;
                }
            }
            
            $result['description'] = call_user_func(array($class, 'getDescription'));
            $result['pluginName'] = call_user_func(array($class, 'getName'));
            $result['pluginClass'] = $class;            
            return $result;
        }
        
        /**
         * Returns the instantiated plugin
         *
         * @param string $pluginName
         * @param int $id Identifier used for identifying a specific plugin instance. 
         * If ommitted will return the first instantiated plugin with the given name.
         * @return iPlugin|null The plugin or null when missing
         */
        public function loadPlugin($pluginName, $id = null)
        {
            // If the id is not set we search for the plugin.
            if (!isset($id))
            {
                foreach ($this->plugins as $plugin)
                {
                    if (get_class($plugin) == $pluginName)
                    {
                        return $plugin;
                    }
                }
            }
            else
            { 
                if ((!isset($this->plugins[$id]) || get_class($this->plugins[$id]) !== $pluginName))
                {
                    if ($this->getPluginInfo($pluginName) !== false) {
                        $this->plugins[$id] = new $pluginName($this, $id);
                    } else {
                        $this->plugins[$id] = null;
                    }
                }
                return $this->plugins[$id];
            }
        }

        /**
         * Handles loading all active plugins
         *
         * Possible improvement would be to load them for a specific context.
         * For instance 'survey' for runtime or 'admin' for backend. This needs
         * some thinking before implementing.
         */
        public function loadPlugins()
        {
            try {
                $pluginModel = Plugin::model();    
                $records = $pluginModel->findAllByAttributes(array('active'=>1));
            
                foreach ($records as $record) {
                    $this->loadPlugin($record->name, $record->id);
                }
            } catch (Exception $exc) {
                // Something went wrong, maybe no database was present so we load no plugins
            }
            
            $this->dispatchEvent(new PluginEvent('afterPluginLoad', $this));    // Alow plugins to do stuff after all plugins are loaded
        }
        
        /**
         * Get a list of question objects and load some information about them.
         * This registers the question object classes with Yii.
         */
        public function loadQuestionObjects($forceReload = false)
        {
            if (empty($this->guidToQuestion) || $forceReload)
            {
                $event = new PluginEvent('listQuestionPlugins');
                $this->dispatchEvent($event);


                foreach ($event->get('questionplugins', array()) as $pluginClass => $paths)
                {
                    foreach ($paths as $path)
                    {

                        Yii::import("webroot.plugins.$pluginClass.$path");
                        $parts = explode('.', $path);

                        // Get the class name.
                        $className = array_pop($parts);

                        // Get the GUID for the question object.
                        $guid = forward_static_call(array($className, 'getGUID'));

                        // Save the GUID-class mapping.
                        $this->guidToQuestion[$guid] = array(
                            'class' => $className,
                            'guid' => $guid,
                            'plugin' => $pluginClass,
                            'name' => $className::$info['name']
                        );
                    }
                }
            }
            
            return $this->guidToQuestion;
        }
        
        /**
         * Construct a question object from a GUID.
         * @param string $guid
         * @param int $questionId,
         * @param int $responseId
         * @return iQuestion
         */
        public function constructQuestionFromGUID($guid, $questionId = null, $responseId = null)
        {
            $this->loadQuestionObjects();
            if (isset($this->guidToQuestion[$guid]))
            {
                $questionClass = $this->guidToQuestion[$guid]['class'];
                $questionObject = new $questionClass($this->loadPlugin($this->guidToQuestion[$guid]['plugin']), $this->api, $questionId, $responseId);
                return $questionObject;
            }
        }
        
        
    }
?>
