<?php
/**
 * Zend Framework (http://framework.zend.com/)
 *
 * @link      http://github.com/zendframework/zf2 for the canonical source repository
 * @copyright Copyright (c) 2005-2013 Zend Technologies USA Inc. (http://www.zend.com)
 * @license   http://framework.zend.com/license/new-bsd New BSD License
 */

namespace Zend\Di;

/**
 * Registry of instantiated objects, their names and the parameters used to build them
 */
class InstanceManager /* implements InstanceManagerInterface */
{
    
/**
     * Array of shared instances
     * @var array
     */
    
protected $sharedInstances = array();

    
/**
     * Array of shared instances with params
     * @var array
     */
    
protected $sharedInstancesWithParams = array('hashShort' => array(), 'hashLong' => array());

    
/**
     * Array of class aliases
     * @var array key: alias, value: class
     */
    
protected $aliases = array();

    
/**
     * The template to use for housing configuration information
     * @var array
     */
    
protected $configurationTemplate = array(
        
/**
         * alias|class => alias|class
         * interface|abstract => alias|class|object
         * name => value
         */
        
'parameters' => array(),
        
/**
         * injection type => array of ordered method params
         */
        
'injections' => array(),
        
/**
         * alias|class => bool
         */
        
'shared' => true
    
);

    
/**
     * An array of instance configuration data
     * @var array
     */
    
protected $configurations = array();

    
/**
     * An array of globally preferred implementations for interfaces/abstracts
     * @var array
     */
    
protected $typePreferences = array();

    
/**
     * Does this instance manager have this shared instance
     * @param  string $classOrAlias
     * @return bool
     */
    
public function hasSharedInstance($classOrAlias)
    {
        return isset(
$this->sharedInstances[$classOrAlias]);
    }

    
/**
     * getSharedInstance()
     */
    
public function getSharedInstance($classOrAlias)
    {
        return 
$this->sharedInstances[$classOrAlias];
    }

    
/**
     * Add shared instance
     *
     * @param  object                             $instance
     * @param  string                             $classOrAlias
     * @throws Exception\InvalidArgumentException
     */
    
public function addSharedInstance($instance$classOrAlias)
    {
        if (!
is_object($instance)) {
            throw new 
Exception\InvalidArgumentException('This method requires an object to be shared. Class or Alias given: ' $classOrAlias);
        }

        
$this->sharedInstances[$classOrAlias] = $instance;
    }

    
/**
     * hasSharedInstanceWithParameters()
     *
     * @param  string      $classOrAlias
     * @param  array       $params
     * @param  bool        $returnFastHashLookupKey
     * @return bool|string
     */
    
public function hasSharedInstanceWithParameters($classOrAlias, array $params$returnFastHashLookupKey false)
    {
        
ksort($params);
        
$hashKey $this->createHashForKeys($classOrAliasarray_keys($params));
        if (isset(
$this->sharedInstancesWithParams['hashShort'][$hashKey])) {
            
$hashValue $this->createHashForValues($classOrAlias$params);
            if (isset(
$this->sharedInstancesWithParams['hashLong'][$hashKey '/' $hashValue])) {
                return (
$returnFastHashLookupKey) ? $hashKey '/' $hashValue true;
            }
        }

        return 
false;
    }

    
/**
     * addSharedInstanceWithParameters()
     *
     * @param  object $instance
     * @param  string $classOrAlias
     * @param  array  $params
     * @return void
     */
    
public function addSharedInstanceWithParameters($instance$classOrAlias, array $params)
    {
        
ksort($params);
        
$hashKey $this->createHashForKeys($classOrAliasarray_keys($params));
        
$hashValue $this->createHashForValues($classOrAlias$params);

        if (!isset(
$this->sharedInstancesWithParams[$hashKey])
            || !
is_array($this->sharedInstancesWithParams[$hashKey])) {
            
$this->sharedInstancesWithParams[$hashKey] = array();
        }

        
$this->sharedInstancesWithParams['hashShort'][$hashKey] = true;
        
$this->sharedInstancesWithParams['hashLong'][$hashKey '/' $hashValue] = $instance;
    }

    
/**
     * Retrieves an instance by its name and the parameters stored at its instantiation
     *
     * @param  string      $classOrAlias
     * @param  array       $params
     * @param  bool|null   $fastHashFromHasLookup
     * @return object|bool false if no instance was found
     */
    
public function getSharedInstanceWithParameters($classOrAlias, array $params$fastHashFromHasLookup null)
    {
        if (
$fastHashFromHasLookup) {
            return 
$this->sharedInstancesWithParams['hashLong'][$fastHashFromHasLookup];
        }

        
ksort($params);
        
$hashKey $this->createHashForKeys($classOrAliasarray_keys($params));
        if (isset(
$this->sharedInstancesWithParams['hashShort'][$hashKey])) {
            
$hashValue $this->createHashForValues($classOrAlias$params);
            if (isset(
$this->sharedInstancesWithParams['hashLong'][$hashKey '/' $hashValue])) {
                return 
$this->sharedInstancesWithParams['hashLong'][$hashKey '/' $hashValue];
            }
        }

        return 
false;
    }

    
/**
     * Check for an alias
     *
     * @param  string $alias
     * @return bool
     */
    
public function hasAlias($alias)
    {
        return (isset(
$this->aliases[$alias]));
    }

    
/**
     * Get aliases
     *
     * @return array
     */
    
public function getAliases()
    {
        return 
$this->aliases;
    }

    
/**
     * getClassFromAlias()
     *
     * @param string
     * @return string|bool
     * @throws Exception\RuntimeException
     */
    
public function getClassFromAlias($alias)
    {
        if (!isset(
$this->aliases[$alias])) {
            return 
false;
        }
        
$r 0;
        while (isset(
$this->aliases[$alias])) {
            
$alias $this->aliases[$alias];
            
$r++;
            if (
$r 100) {
                throw new 
Exception\RuntimeException(
                    
sprintf('Possible infinite recursion in DI alias! Max recursion of 100 levels reached at alias "%s".'$alias)
                );
            }
        }

        return 
$alias;
    }

    
/**
     * @param  string                     $alias
     * @return string|bool
     * @throws Exception\RuntimeException
     */
    
protected function getBaseAlias($alias)
    {
        if (!
$this->hasAlias($alias)) {
            return 
false;
        }
        
$lastAlias false;
        
$r 0;
        while (isset(
$this->aliases[$alias])) {
            
$lastAlias $alias;
            
$alias $this->aliases[$alias];
            
$r++;
            if (
$r 100) {
                throw new 
Exception\RuntimeException(
                    
sprintf('Possible infinite recursion in DI alias! Max recursion of 100 levels reached at alias "%s".'$alias)
                );
            }
        }

        return 
$lastAlias;
    }

    
/**
     * Add alias
     *
     * @throws Exception\InvalidArgumentException
     * @param  string                             $alias
     * @param  string                             $class
     * @param  array                              $parameters
     * @return void
     */
    
public function addAlias($alias$class, array $parameters = array())
    {
        if (!
preg_match('#^[a-zA-Z0-9-_]+$#'$alias)) {
            throw new 
Exception\InvalidArgumentException(
                
'Aliases must be alphanumeric and can contain dashes and underscores only.'
            
);
        }
        
$this->aliases[$alias] = $class;
        if (
$parameters) {
            
$this->setParameters($alias$parameters);
        }
    }

    
/**
     * Check for configuration
     *
     * @param  string $aliasOrClass
     * @return bool
     */
    
public function hasConfig($aliasOrClass)
    {
        
$key = ($this->hasAlias($aliasOrClass)) ? 'alias:' $this->getBaseAlias($aliasOrClass) : $aliasOrClass;
        if (!isset(
$this->configurations[$key])) {
            return 
false;
        }
        if (
$this->configurations[$key] === $this->configurationTemplate) {
            return 
false;
        }

        return 
true;
    }

    
/**
     * Sets configuration for a single alias/class
     *
     * @param string $aliasOrClass
     * @param array  $configuration
     * @param bool   $append
     */
    
public function setConfig($aliasOrClass, array $configuration$append false)
    {
        
$key = ($this->hasAlias($aliasOrClass)) ? 'alias:' $this->getBaseAlias($aliasOrClass) : $aliasOrClass;
        if (!isset(
$this->configurations[$key]) || !$append) {
            
$this->configurations[$key] = $this->configurationTemplate;
        }
        
// Ignore anything but 'parameters' and 'injections'
        
$configuration = array(
            
'parameters' => isset($configuration['parameters']) ? $configuration['parameters'] : array(),
            
'injections' => isset($configuration['injections']) ? $configuration['injections'] : array(),
            
'shared'     => isset($configuration['shared'])     ? $configuration['shared']     : true
        
);
        
$this->configurations[$key] = array_replace_recursive($this->configurations[$key], $configuration);
    }

    
/**
     * Get classes
     *
     * @return array
     */
    
public function getClasses()
    {
        
$classes = array();
        foreach (
$this->configurations as $name => $data) {
            if (
strpos($name'alias') === 0) continue;
            
$classes[] = $name;
        }

        return 
$classes;
    }

    
/**
     * @param  string $aliasOrClass
     * @return array
     */
    
public function getConfig($aliasOrClass)
    {
        
$key = ($this->hasAlias($aliasOrClass)) ? 'alias:' $this->getBaseAlias($aliasOrClass) : $aliasOrClass;
        if (isset(
$this->configurations[$key])) {
            return 
$this->configurations[$key];
        }

        return 
$this->configurationTemplate;
    }

    
/**
     * setParameters() is a convenience method for:
     *    setConfig($type, array('parameters' => array(...)), true);
     *
     * @param  string $aliasOrClass Alias or Class
     * @param  array  $parameters   Multi-dim array of parameters and their values
     * @return void
     */
    
public function setParameters($aliasOrClass, array $parameters)
    {
        
$this->setConfig($aliasOrClass, array('parameters' => $parameters), true);
    }

    
/**
     * setInjections() is a convenience method for:
     *    setConfig($type, array('injections' => array(...)), true);
     *
     * @param  string $aliasOrClass Alias or Class
     * @param  array  $injections   Multi-dim array of methods and their parameters
     * @return void
     */
    
public function setInjections($aliasOrClass, array $injections)
    {
        
$this->setConfig($aliasOrClass, array('injections' => $injections), true);
    }

    
/**
     * Set shared
     *
     * @param  string $aliasOrClass
     * @param  bool   $isShared
     * @return void
     */
    
public function setShared($aliasOrClass$isShared)
    {
        
$this->setConfig($aliasOrClass, array('shared' => (bool) $isShared), true);
    }

    
/**
     * Check for type preferences
     *
     * @param  string $interfaceOrAbstract
     * @return bool
     */
    
public function hasTypePreferences($interfaceOrAbstract)
    {
        
$key = ($this->hasAlias($interfaceOrAbstract)) ? 'alias:' $interfaceOrAbstract $interfaceOrAbstract;

        return (isset(
$this->typePreferences[$key]) && $this->typePreferences[$key]);
    }

    
/**
     * Set type preference
     *
     * @param  string          $interfaceOrAbstract
     * @param  array           $preferredImplementations
     * @return InstanceManager
     */
    
public function setTypePreference($interfaceOrAbstract, array $preferredImplementations)
    {
        
$key = ($this->hasAlias($interfaceOrAbstract)) ? 'alias:' $interfaceOrAbstract $interfaceOrAbstract;
        foreach (
$preferredImplementations as $preferredImplementation) {
            
$this->addTypePreference($key$preferredImplementation);
        }

        return 
$this;
    }

    
/**
     * Get type preferences
     *
     * @param  string $interfaceOrAbstract
     * @return array
     */
    
public function getTypePreferences($interfaceOrAbstract)
    {
        
$key = ($this->hasAlias($interfaceOrAbstract)) ? 'alias:' $interfaceOrAbstract $interfaceOrAbstract;
        if (isset(
$this->typePreferences[$key])) {
            return 
$this->typePreferences[$key];
        }

        return array();
    }

    
/**
     * Unset type preferences
     *
     * @param  string $interfaceOrAbstract
     * @return void
     */
    
public function unsetTypePreferences($interfaceOrAbstract)
    {
        
$key = ($this->hasAlias($interfaceOrAbstract)) ? 'alias:' $interfaceOrAbstract $interfaceOrAbstract;
        unset(
$this->typePreferences[$key]);
    }

    
/**
     * Adds a type preference. A type preference is a redirection to a preferred alias or type when an abstract type
     * $interfaceOrAbstract is requested
     *
     * @param  string $interfaceOrAbstract
     * @param  string $preferredImplementation
     * @return self
     */
    
public function addTypePreference($interfaceOrAbstract$preferredImplementation)
    {
        
$key = ($this->hasAlias($interfaceOrAbstract)) ? 'alias:' $interfaceOrAbstract $interfaceOrAbstract;
        if (!isset(
$this->typePreferences[$key])) {
            
$this->typePreferences[$key] = array();
        }
        
$this->typePreferences[$key][] = $preferredImplementation;

        return 
$this;
    }

    
/**
     * Removes a previously set type preference
     *
     * @param  string    $interfaceOrAbstract
     * @param  string    $preferredType
     * @return bool|self
     */
    
public function removeTypePreference($interfaceOrAbstract$preferredType)
    {
        
$key = ($this->hasAlias($interfaceOrAbstract)) ? 'alias:' $interfaceOrAbstract $interfaceOrAbstract;
        if (!isset(
$this->typePreferences[$key]) || !in_array($preferredType$this->typePreferences[$key])) {
            return 
false;
        }
        unset(
$this->typePreferences[$key][array_search($key$this->typePreferences)]);

        return 
$this;
    }

    
/**
     * @param  string   $classOrAlias
     * @param  string[] $paramKeys
     * @return string
     */
    
protected function createHashForKeys($classOrAlias$paramKeys)
    {
        return 
$classOrAlias ':' implode('|'$paramKeys);
    }

    
/**
     * @param  string $classOrAlias
     * @param  array  $paramValues
     * @return string
     */
    
protected function createHashForValues($classOrAlias$paramValues)
    {
        
$hashValue '';
        foreach (
$paramValues as $param) {
            switch (
gettype($param)) {
                case 
'object':
                    
$hashValue .= spl_object_hash($param) . '|';
                    break;
                case 
'integer':
                case 
'string':
                case 
'boolean':
                case 
'NULL':
                case 
'double':
                    
$hashValue .= $param '|';
                    break;
                case 
'array':
                    
$hashValue .= 'Array|';
                    break;
                case 
'resource':
                    
$hashValue .= 'resource|';
                    break;
            }
        }

        return 
$hashValue;
    }
}