<?php
/**
 * core.framework
 *
 * @category   Core
 * @package    Core_Plugin
 * @copyright  Copyright (c) 2011. Burza d.o.o. (http://web.burza.hr/en/)
 * @license    proprietary
 */

/**
 * @category  Core
 * @package   Core_Plugin
 * @copyright Copyright (c) 2011. Burza d.o.o. (http://web.burza.hr/en/)
 * @license   proprietary
 */
class Core_Plugin_Manager
{
    const NAME_PLUGIN     = 'Plugin';
    const NAME_PLUGINS    = 'Plugins';

    /**
     * @var Core_Loader_PluginLoader
     */
    protected $_pluginLoader;

    /**
     * @var array
     */
    protected $_types     = array();

    /**
     * @var array
     */
    protected $_instances = array();

    /**
     * @var string
     */
    protected $_template  = '/^(?<action>add|create|get|has|remove)(?<name>.+)$/';

    /**
     * @var Core_Plugin_Interface
     */
    protected $_owner;
    
    /**
     * @var array
     */
    protected $_ownedInstances = array();

    /**
     * @param array $types
     */
    public function __construct(Core_Plugin_Interface $owner, array $types = null)
    {
        $this->setOwner($owner);

        if (null !== $types) {
            $this->addTypes($types);
        }
    }

    /**
     * @param type $class
     *
     * @return type
     */
    public function hasType($class)
    {
        return array_key_exists($class, $this->_types);
    }

    /**
     * @param array $types
     *
     * @return \Core_Plugin_Manager
     */
    public function addTypes(array $types)
    {
        foreach ($types as $class => $config) {
            list($type, $options) = $config;
            $this->addType($class, $type, $options);
        }
        return $this;
    }

    /**
     * @param string $class
     * @param string $type
     * @param array  $options
     *
     * @return \Core_Plugin_Manager
     */
    public function addType($class, $type, array $options)
    {
        if (!isset($options['uniqueAcross'])) {
            $options['uniqueAcross']   = array($class);
        } else if (!in_array($class, $options['uniqueAcross'])) {
            $options['uniqueAcross'][] = $class;
        }

        $this->_types[$class]          = compact('type', 'options');
        $this->_instances[$class]      = array();
        $this->_ownedInstances[$class] = array();
        return $this;
    }

    /**
     * @param string $class
     *
     * @return \Core_Plugin_Manager
     * @throws InvalidArgumentException
     */
    public function removeType($class)
    {
        if (!$this->hasType($class)) {
            $message = sprintf('Failed removing type %s, not managed', $class);
            throw new InvalidArgumentException($message);
        }

        unset($this->_types[$class]);
        unset($this->_instances[$class]);
        return $this;
    }

    /**
     * @param Core_Plugin_Interface $owner
     *
     * @return \Core_Plugin_Manager
     */
    public function setOwner(Core_Plugin_Interface $owner)
    {
        $this->_owner = $owner;

        // also change owner on all instances
        foreach($this->_instances as $instances) {
            foreach($instances as $instance) {
                $instance->setOwner($owner);
            }
        }

        return $this;
    }

    /**
     * @return Core_Plugin_Manager
     */
    public function getOwner()
    {
        return $this->_owner;
    }

    /**
     * @param Core_Loader_PluginLoader $pluginLoader
     *
     * @return Core_Plugin_Manager
     */
    public function setPluginLoader(Core_Loader_PluginLoader $pluginLoader)
    {
        $this->_pluginLoader = $pluginLoader;
        return $this;
    }

    /**
     * @return Core_Loader_PluginLoader
     */
    public function getPluginLoader()
    {
        if (null === $this->_pluginLoader) {
            $this->_pluginLoader = Core_Application::get('PluginLoader');
        }
        return $this->_pluginLoader;
    }

    /**
     * @param string $class   Plugin declared class name
     * @param array  $plugins Plugin instances or specs
     *
     * @return \Core_Plugin_Manager
     * @throws InvalidArgumentException
     */
    public function addPlugins($class, array $plugins)
    {
        if (!$this->hasType($class)) {
            $message = sprintf('Failed adding plugins, passed type %s is not managed', $class);
            throw new InvalidArgumentException($message);
        }

        foreach($plugins as $name => $plugin) {
            if ($plugin instanceof $class) {
                $this->addPlugin($class, $plugin);
            } else if (is_array($plugin)) {
                // plugin type
                if (!array_key_exists('type', $plugin)) {
                    $message = sprintf('Failed adding plugin %s, no "type" specified', $name);
                    throw new InvalidArgumentException($message);
                }
                $type = $plugin['type'];
                unset($plugin['type']);

                $options = array('owner' => $this->getOwner());
                if (array_key_exists('options', $plugin)) {
                    $options += $plugin['options'];
                }

                $this->createPlugin($class, $type, $name, $options);
            } else {
                $message = sprintf('Failed adding plugin %s, not an instance of %s or array', $name, $class);
                throw new InvalidArgumentException($message);
            }
        }
        return $this;
    }

    /**
     * @param string $class   Plugin declared class name
     * @param string $type    Plugin type
     * @param string $name    Plugin name
     * @param array  $options Plugin options
     *
     * @return Core_Plugin_Interface
     */
    public function createPlugin($class, $type, $name, array $options = null)
    {
        $options = (array) $options + compact('name');
        $autoAdd = true;
        if (isset($options['__autoAdd'])) {
            $autoAdd = (bool) $options['__autoAdd'];
            unset($options['__autoAdd']);
        }
        $type    = $this->_types[$class]['type'] . trim(ucfirst($type));
        $plugin  = $this->getPluginLoader()->initializePlugin($type, $options);
        if (true === $autoAdd) {
            $this->addPlugin($class, $plugin);            
        }
        return $plugin;
    }

    /**
     * @param string                $class  Plugin declared class name
     * @param Core_Plugin_Interface $plugin Plugin instance
     * @param boolean               $owned  Is this plugin owned by the owner or is it managed in behalf od someone else?
     *
     * @return \Core_Plugin_Manager
     * @throws InvalidArgumentException
     */
    public function addPlugin($class, Core_Plugin_Interface $plugin, $owned = true)
    {
        $name = $plugin->getName();
        if (!$this->hasType($class)) {
            $message = sprintf('Failed adding plugin %s, passed type %s is not managed', $name, $class);
            throw new InvalidArgumentException($message);
        } else  if (!$plugin instanceof $class) {
            $message = sprintf('Failed adding plugin %s, must be instance of %s, %s given', $name, $class, get_class($plugin));
            throw new InvalidArgumentException($message);
        }

        // plugin name must be unique across all of these types
        $uniqueAcross = $this->_types[$class]['options']['uniqueAcross'];
        foreach($uniqueAcross as $linkedClass) {
            if ($this->hasPlugin($linkedClass, $name)) {
                $message = sprintf('Failed adding plugin "%s", name already used by plugin of type %s', $name, $linkedClass);
                throw new InvalidArgumentException($message);
            }
        }
        $plugin->setOwner($this->getOwner());
        $plugin->init();
        
        $normalizedName = strtolower($name);
        if (true === $owned) {
            $this->_ownedInstances[$class][$normalizedName] = true;
        }

        $this->_instances[$class][$normalizedName] = $plugin;
        return $this;
    }

    /**
     * @param string $class Plugin declared class name
     * @param string $name  Plugin name
     *
     * @return Core_Plugin_Interface
     * @throws InvalidArgumentException
     */
    public function getPlugin($class, $name)
    {
        if (!$this->hasPlugin($class, $name)) {
            $message = sprintf('Failed fetching plugin %s, no such plugin found', $name);
            throw new InvalidArgumentException($message);
        }
        return $this->_instances[$class][strtolower($name)];
    }

    /**
     * @param string  $class Plugin declared class name
     * @param boolean $spec  We only wish to return owned plugins
     *
     * @return array(Core_Plugin_Interface)
     * @throws InvalidArgumentException
     */
    public function getPlugins($class, $spec = true)
    {
        if (!$this->hasType($class)) {
            $message = sprintf('Failed fetching all plugins, passed type %s is not managed', $class);
            throw new InvalidArgumentException($message);
        }
        
        if (true === $spec) {
            return array_intersect_key($this->_instances[$class], $this->_ownedInstances[$class]);
        } else if (is_array($spec)) {
            return array_intersect_key($this->_instances[$class], array_flip($spec));
        }
        return $this->_instances[$class];
    }

    /**
     * @param string $class Plugin declared class name
     * @param string $name  Plugin name
     *
     * @return boolean
     * @throws InvalidArgumentException
     */
    public function hasPlugin($class, $name)
    {
        if (!$this->hasType($class)) {
            $message = sprintf('Failed checking if we have plugin, passed type %s is not managed', $class);
            throw new InvalidArgumentException($message);
        }
        return (array_key_exists(strtolower($name), $this->_instances[$class]));
    }

    /**
     * @param string $class Plugin declared class name
     * @param string $name  Plugin name
     *
     * @return \Core_Plugin_Manager
     * @throws InvalidArgumentException
     */
    public function removePlugin($class, $name)
    {
        if (!$this->hasPlugin($class, $name)) {
            $message = sprintf('Failed removing plugin %s, no such plugin found', $name);
            throw new InvalidArgumentException($message);
        }
        $normalizedName = strtolower($name);
        unset($this->_instances[$class][$normalizedName]);
        if (isset($this->_ownedInstances[$class][$normalizedName])) {
            unset($this->_ownedInstances[$class][$normalizedName]);
        }
        return $this;
    }

    /**
     * @param string $name Method name
     *
     * @return boolean
     */
    public function isManaged($name)
    {
        $matches  = array();
        if (preg_match($this->_template, strtolower($name), $matches)) {
            $name = ucfirst($matches['name']);
            foreach($this->_types as $type) {
                if (array_key_exists($name, $type['options']['mapping'])) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * @param string $name   Method name
     * @param array  $params Method params
     *
     * @return mixed|boolean
     */
    public function manage($name, array $params)
    {
        $matches  = array();
        if (!preg_match($this->_template, strtolower($name), $matches)) {
            return;
        }
        $name   = ucfirst($matches['name']);
        foreach($this->_types as $class => $type) {
            if (array_key_exists($name, $type['options']['mapping'])) {
                $method = $matches['action'] . $type['options']['mapping'][$name];
                array_unshift($params, $class);
                return call_user_func_array(array($this, $method), $params);
            }
        }
    }

    /**
     * @return void
     */
    public function __clone()
    {
        foreach($this->_instances as $type => $instances) {
            foreach($instances as $idx => $instance) {
                $this->_instances[$type][$idx] = clone $instance;
            }
        }
    }
}
