<?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\ServiceLocator;

use 
Zend\Code\Generator\ClassGenerator;
use 
Zend\Code\Generator\FileGenerator;
use 
Zend\Code\Generator\MethodGenerator;
use 
Zend\Code\Generator\ParameterGenerator;
use 
Zend\Di\Di;
use 
Zend\Di\Exception;

/**
 * Generator that creates the body of a service locator that can emulate the logic of the given Zend\Di\Di instance
 * without class definitions
 */
class Generator
{
    protected 
$containerClass 'ApplicationContext';

    
/** @var DependencyInjectorProxy */
    
protected $injector;

    
/**
     * @var null|string
     */
    
protected $namespace;

    
/**
     * Constructor
     *
     * Requires a DependencyInjection manager on which to operate.
     *
     * @param Di $injector
     */
    
public function __construct(Di $injector)
    {
        
$this->injector = new DependencyInjectorProxy($injector);
    }

    
/**
     * Set the class name for the generated service locator container
     *
     * @param  string    $name
     * @return Generator
     */
    
public function setContainerClass($name)
    {
        
$this->containerClass $name;

        return 
$this;
    }

    
/**
     * Set the namespace to use for the generated class file
     *
     * @param  string    $namespace
     * @return Generator
     */
    
public function setNamespace($namespace)
    {
        
$this->namespace $namespace;

        return 
$this;
    }

    
/**
     * Construct, configure, and return a PHP class file code generation object
     *
     * Creates a Zend\Code\Generator\FileGenerator object that has
     * created the specified class and service locator methods.
     *
     * @param  null|string                         $filename
     * @throws \Zend\Di\Exception\RuntimeException
     * @return FileGenerator
     */
    
public function getCodeGenerator($filename null)
    {
        
$injector       $this->injector;
        
$im             $injector->instanceManager();
        
$indent         '    ';
        
$aliases        $this->reduceAliases($im->getAliases());
        
$caseStatements = array();
        
$getters        = array();
        
$definitions    $injector->definitions();

        
$fetched array_unique(array_merge($definitions->getClasses(), $im->getAliases()));

        foreach (
$fetched as $name) {
            
$getter $this->normalizeAlias($name);
            
$meta   $injector->get($name);
            
$params $meta->getParams();

            
// Build parameter list for instantiation
            
foreach ($params as $key => $param) {
                if (
null === $param || is_scalar($param) || is_array($param)) {
                    
$string var_export($param1);
                    if (
strstr($string'::__set_state(')) {
                        throw new 
Exception\RuntimeException('Arguments in definitions may not contain objects');
                    }
                    
$params[$key] = $string;
                } elseif (
$param instanceof GeneratorInstance) {
                    
/* @var $param GeneratorInstance */
                    
$params[$key] = sprintf('$this->%s()'$this->normalizeAlias($param->getName()));
                } else {
                    
$message sprintf('Unable to use object arguments when building containers. Encountered with "%s", parameter of type "%s"'$nameget_class($param));
                    throw new 
Exception\RuntimeException($message);
                }
            }

            
// Strip null arguments from the end of the params list
            
$reverseParams array_reverse($paramstrue);
            foreach (
$reverseParams as $key => $param) {
                if (
'NULL' === $param) {
                    unset(
$params[$key]);
                    continue;
                }
                break;
            }

            
// Create instantiation code
            
$constructor $meta->getConstructor();
            if (
'__construct' != $constructor) {
                
// Constructor callback
                
$callback var_export($constructor1);
                if (
strstr($callback'::__set_state(')) {
                    throw new 
Exception\RuntimeException('Unable to build containers that use callbacks requiring object instances');
                }
                if (
count($params)) {
                    
$creation sprintf('$object = call_user_func(%s, %s);'$callbackimplode(', '$params));
                } else {
                    
$creation sprintf('$object = call_user_func(%s);'$callback);
                }
            } else {
                
// Normal instantiation
                
$className '\\' ltrim($name'\\');
                
$creation sprintf('$object = new %s(%s);'$classNameimplode(', '$params));
            }

            
// Create method call code
            
$methods '';
            foreach (
$meta->getMethods() as $methodData) {
                if (!isset(
$methodData['name']) && !isset($methodData['method'])) {
                    continue;
                }
                
$methodName   = isset($methodData['name']) ? $methodData['name'] : $methodData['method'];
                
$methodParams $methodData['params'];

                
// Create method parameter representation
                
foreach ($methodParams as $key => $param) {
                    if (
null === $param || is_scalar($param) || is_array($param)) {
                        
$string var_export($param1);
                        if (
strstr($string'::__set_state(')) {
                            throw new 
Exception\RuntimeException('Arguments in definitions may not contain objects');
                        }
                        
$methodParams[$key] = $string;
                    } elseif (
$param instanceof GeneratorInstance) {
                        
$methodParams[$key] = sprintf('$this->%s()'$this->normalizeAlias($param->getName()));
                    } else {
                        
$message sprintf('Unable to use object arguments when generating method calls. Encountered with class "%s", method "%s", parameter of type "%s"'$name$methodNameget_class($param));
                        throw new 
Exception\RuntimeException($message);
                    }
                }

                
// Strip null arguments from the end of the params list
                
$reverseParams array_reverse($methodParamstrue);
                foreach (
$reverseParams as $key => $param) {
                    if (
'NULL' === $param) {
                        unset(
$methodParams[$key]);
                        continue;
                    }
                    break;
                }

                
$methods .= sprintf("\$object->%s(%s);\n"$methodNameimplode(', '$methodParams));
            }

            
// Generate caching statement
            
$storage '';
            if (
$im->hasSharedInstance($name$params)) {
                
$storage sprintf("\$this->services['%s'] = \$object;\n"$name);
            }

            
// Start creating getter
            
$getterBody '';

            
// Create fetch of stored service
            
if ($im->hasSharedInstance($name$params)) {
                
$getterBody .= sprintf("if (isset(\$this->services['%s'])) {\n"$name);
                
$getterBody .= sprintf("%sreturn \$this->services['%s'];\n}\n\n"$indent$name);
            }

            
// Creation and method calls
            
$getterBody .= sprintf("%s\n"$creation);
            
$getterBody .= $methods;

            
// Stored service
            
$getterBody .= $storage;

            
// End getter body
            
$getterBody .= "return \$object;\n";

            
$getterDef = new MethodGenerator();
            
$getterDef->setName($getter);
            
$getterDef->setBody($getterBody);
            
$getters[] = $getterDef;

            
// Get cases for case statements
            
$cases = array($name);
            if (isset(
$aliases[$name])) {
                
$cases array_merge($aliases[$name], $cases);
            }

            
// Build case statement and store
            
$statement '';
            foreach (
$cases as $value) {
                
$statement .= sprintf("%scase '%s':\n"$indent$value);
            }
            
$statement .= sprintf("%sreturn \$this->%s();\n"str_repeat($indent2), $getter);

            
$caseStatements[] = $statement;
        }

        
// Build switch statement
        
$switch  sprintf("switch (%s) {\n%s\n"'$name'implode("\n"$caseStatements));
        
$switch .= sprintf("%sdefault:\n%sreturn parent::get(%s, %s);\n"$indentstr_repeat($indent2), '$name''$params');
        
$switch .= "}\n\n";

        
// Build get() method
        
$nameParam   = new ParameterGenerator();
        
$nameParam->setName('name');
        
$paramsParam = new ParameterGenerator();
        
$paramsParam->setName('params')
                    ->
setType('array')
                    ->
setDefaultValue(array());

        
$get = new MethodGenerator();
        
$get->setName('get');
        
$get->setParameters(array(
            
$nameParam,
            
$paramsParam,
        ));
        
$get->setBody($switch);

        
// Create getters for aliases
        
$aliasMethods = array();
        foreach (
$aliases as $class => $classAliases) {
            foreach (
$classAliases as $alias) {
                
$aliasMethods[] = $this->getCodeGenMethodFromAlias($alias$class);
            }
        }

        
// Create class code generation object
        
$container = new ClassGenerator();
        
$container->setName($this->containerClass)
                  ->
setExtendedClass('ServiceLocator')
                  ->
addMethodFromGenerator($get)
                  ->
addMethods($getters)
                  ->
addMethods($aliasMethods);

        
// Create PHP file code generation object
        
$classFile = new FileGenerator();
        
$classFile->setUse('Zend\Di\ServiceLocator')
                  ->
setClass($container);

        if (
null !== $this->namespace) {
            
$classFile->setNamespace($this->namespace);
        }

        if (
null !== $filename) {
            
$classFile->setFilename($filename);
        }

        return 
$classFile;
    }

    
/**
     * Reduces aliases
     *
     * Takes alias list and reduces it to a 2-dimensional array of
     * class names pointing to an array of aliases that resolve to
     * it.
     *
     * @param  array $aliasList
     * @return array
     */
    
protected function reduceAliases(array $aliasList)
    {
        
$reduced = array();
        
$aliases array_keys($aliasList);
        foreach (
$aliasList as $alias => $service) {
            if (
in_array($service$aliases)) {
                do {
                    
$service $aliasList[$service];
                } while (
in_array($service$aliases));
            }
            if (!isset(
$reduced[$service])) {
                
$reduced[$service] = array();
            }
            
$reduced[$service][] = $alias;
        }

        return 
$reduced;
    }

    
/**
     * Create a PhpMethod code generation object named after a given alias
     *
     * @param  string          $alias
     * @param  string          $class Class to which alias refers
     * @return MethodGenerator
     */
    
protected function getCodeGenMethodFromAlias($alias$class)
    {
        
$alias $this->normalizeAlias($alias);
        
$method = new MethodGenerator();
        
$method->setName($alias);
        
$method->setBody(sprintf('return $this->get(\'%s\');'$class));

        return 
$method;
    }

    
/**
     * Normalize an alias to a getter method name
     *
     * @param  string $alias
     * @return string
     */
    
protected function normalizeAlias($alias)
    {
        
$normalized preg_replace('/[^a-zA-Z0-9]/'' '$alias);
        
$normalized 'get' str_replace(' '''ucwords($normalized));

        return 
$normalized;
    }
}