<?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\Server\Reflection;

use 
ReflectionFunctionAbstract;
use 
ReflectionClass as PhpReflectionClass;
use 
ReflectionFunction as PhpReflectionFunction;
use 
ReflectionMethod as PhpReflectionMethod;
use 
Zend\Code\Reflection\DocBlockReflection;

/**
 * Function/Method Reflection
 *
 * Decorates a ReflectionFunction. Allows setting and retrieving an alternate
 * 'service' name (i.e., the name to be used when calling via a service),
 * setting and retrieving the description (originally set using the docblock
 * contents), retrieving the callback and callback type, retrieving additional
 * method invocation arguments, and retrieving the
 * method {@link \Zend\Server\Reflection\Prototype prototypes}.
 */
abstract class AbstractFunction
{
    
/**
     * @var ReflectionFunctionAbstract
     */
    
protected $reflection;

    
/**
     * Additional arguments to pass to method on invocation
     * @var array
     */
    
protected $argv = array();

    
/**
     * Used to store extra configuration for the method (typically done by the
     * server class, e.g., to indicate whether or not to instantiate a class).
     * Associative array; access is as properties via {@link __get()} and
     * {@link __set()}
     * @var array
     */
    
protected $config = array();

    
/**
     * Declaring class (needed for when serialization occurs)
     * @var string
     */
    
protected $class;

    
/**
     * Function/method description
     * @var string
     */
    
protected $description '';

    
/**
     * Namespace with which to prefix function/method name
     * @var string
     */
    
protected $namespace;

    
/**
     * Prototypes
     * @var array
     */
    
protected $prototypes = array();

    private 
$return;
    private 
$returnDesc;
    private 
$paramDesc;
    private 
$sigParams;
    private 
$sigParamsDepth;

    
/**
     * Constructor
     *
     * @param ReflectionFunctionAbstract $r
     * @param null|string $namespace
     * @param null|array $argv
     * @throws Exception\InvalidArgumentException
     * @throws Exception\RuntimeException
     */
    
public function __construct(ReflectionFunctionAbstract $r$namespace null$argv = array())
    {
        
$this->reflection $r;

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

        
// Determine arguments
        
if (is_array($argv)) {
            
$this->argv $argv;
        }

        
// If method call, need to store some info on the class
        
if ($r instanceof PhpReflectionMethod) {
            
$this->class $r->getDeclaringClass()->getName();
        }

        
// Perform some introspection
        
$this->reflect();
    }

    
/**
     * Create signature node tree
     *
     * Recursive method to build the signature node tree. Increments through
     * each array in {@link $sigParams}, adding every value of the next level
     * to the current value (unless the current value is null).
     *
     * @param \Zend\Server\Reflection\Node $parent
     * @param int $level
     * @return void
     */
    
protected function addTree(Node $parent$level 0)
    {
        if (
$level >= $this->sigParamsDepth) {
            return;
        }

        foreach (
$this->sigParams[$level] as $value) {
            
$node = new Node($value$parent);
            if ((
null !== $value) && ($this->sigParamsDepth $level 1)) {
                
$this->addTree($node$level 1);
            }
        }
    }

    
/**
     * Build the signature tree
     *
     * Builds a signature tree starting at the return values and descending
     * through each method argument. Returns an array of
     * {@link \Zend\Server\Reflection\Node}s.
     *
     * @return array
     */
    
protected function buildTree()
    {
        
$returnTree = array();
        foreach (
$this->return as $value) {
            
$node = new Node($value);
            
$this->addTree($node);
            
$returnTree[] = $node;
        }

        return 
$returnTree;
    }

    
/**
     * Build method signatures
     *
     * Builds method signatures using the array of return types and the array of
     * parameters types
     *
     * @param array $return Array of return types
     * @param string $returnDesc Return value description
     * @param array $paramTypes Array of arguments (each an array of types)
     * @param array $paramDesc Array of parameter descriptions
     * @return array
     */
    
protected function buildSignatures($return$returnDesc$paramTypes$paramDesc)
    {
        
$this->return         $return;
        
$this->returnDesc     $returnDesc;
        
$this->paramDesc      $paramDesc;
        
$this->sigParams      $paramTypes;
        
$this->sigParamsDepth count($paramTypes);
        
$signatureTrees       $this->buildTree();
        
$signatures           = array();

        
$endPoints = array();
        foreach (
$signatureTrees as $root) {
            
$tmp $root->getEndPoints();
            if (empty(
$tmp)) {
                
$endPoints array_merge($endPoints, array($root));
            } else {
                
$endPoints array_merge($endPoints$tmp);
            }
        }

        foreach (
$endPoints as $node) {
            if (!
$node instanceof Node) {
                continue;
            }

            
$signature = array();
            do {
                
array_unshift($signature$node->getValue());
                
$node $node->getParent();
            } while (
$node instanceof Node);

            
$signatures[] = $signature;
        }

        
// Build prototypes
        
$params $this->reflection->getParameters();
        foreach (
$signatures as $signature) {
            
$return = new ReflectionReturnValue(array_shift($signature), $this->returnDesc);
            
$tmp    = array();
            foreach (
$signature as $key => $type) {
                
$param = new ReflectionParameter($params[$key], $type, (isset($this->paramDesc[$key]) ? $this->paramDesc[$key] : null));
                
$param->setPosition($key);
                
$tmp[] = $param;
            }

            
$this->prototypes[] = new Prototype($return$tmp);
        }
    }

    
/**
     * Use code reflection to create method signatures
     *
     * Determines the method help/description text from the function DocBlock
     * comment. Determines method signatures using a combination of
     * ReflectionFunction and parsing of DocBlock @param and @return values.
     *
     * @throws Exception\RuntimeException
     * @return array
     */
    
protected function reflect()
    {
        
$function   $this->reflection;
        
$paramCount $function->getNumberOfParameters();
        
$parameters $function->getParameters();
        
$scanner    = new DocBlockReflection(($function->getDocComment()) ? : '/***/');
        
$helpText   $scanner->getLongDescription();
        
/* @var \Zend\Code\Reflection\DocBlock\Tag\ParamTag[] $paramTags */
        
$paramTags $scanner->getTags('param');
        
/* @var \Zend\Code\Reflection\DocBlock\Tag\ReturnTag $returnTag */
        
$returnTag $scanner->getTag('return');

        if (empty(
$helpText)) {
            
$helpText $scanner->getShortDescription();
            if (empty(
$helpText)) {
                
$helpText $function->getName();
            }
        }
        
$this->setDescription($helpText);

        if (
$returnTag) {
            
$return     = array();
            
$returnDesc $returnTag->getDescription();
            foreach (
$returnTag->getTypes() as $type) {
                
$return[] = $type;
            }
        } else {
            
$return     = array('void');
            
$returnDesc '';
        }

        
$paramTypesTmp = array();
        
$paramDesc     = array();
        if (empty(
$paramTags)) {
            foreach (
$parameters as $param) {
                
$paramTypesTmp[] = array(($param->isArray()) ? 'array' 'mixed');
                
$paramDesc[]     = '';
            }
        } else {
            
$paramDesc = array();
            foreach (
$paramTags as $paramTag) {
                
$paramTypesTmp[] = $paramTag->getTypes();
                
$paramDesc[]     = ($paramTag->getDescription()) ? : '';
            }
        }

        
// Get all param types as arrays
        
$nParamTypesTmp count($paramTypesTmp);
        if (
$nParamTypesTmp $paramCount) {
            
$start $paramCount $nParamTypesTmp;
            for (
$i $start$i $paramCount; ++$i) {
                
$paramTypesTmp[$i] = array('mixed');
                
$paramDesc[$i]     = '';
            }
        } elseif (
$nParamTypesTmp != $paramCount) {
            throw new 
Exception\RuntimeException(
               
'Variable number of arguments is not supported for services (except optional parameters). '
             
'Number of function arguments must correspond to actual number of arguments described in a docblock.');
        }

        
$paramTypes = array();
        foreach (
$paramTypesTmp as $i => $param) {
            if (
$parameters[$i]->isOptional()) {
                
array_unshift($paramnull);
            }
            
$paramTypes[] = $param;
        }

        
$this->buildSignatures($return$returnDesc$paramTypes$paramDesc);
    }


    
/**
     * Proxy reflection calls
     *
     * @param string $method
     * @param array $args
     * @throws Exception\BadMethodCallException
     * @return mixed
     */
    
public function __call($method$args)
    {
        if (
method_exists($this->reflection$method)) {
            return 
call_user_func_array(array($this->reflection$method), $args);
        }

        throw new 
Exception\BadMethodCallException('Invalid reflection method ("' $method '")');
    }

    
/**
     * Retrieve configuration parameters
     *
     * Values are retrieved by key from {@link $config}. Returns null if no
     * value found.
     *
     * @param string $key
     * @return mixed
     */
    
public function __get($key)
    {
        if (isset(
$this->config[$key])) {
            return 
$this->config[$key];
        }

        return 
null;
    }

    
/**
     * Set configuration parameters
     *
     * Values are stored by $key in {@link $config}.
     *
     * @param string $key
     * @param mixed $value
     * @return void
     */
    
public function __set($key$value)
    {
        
$this->config[$key] = $value;
    }

    
/**
     * Set method's namespace
     *
     * @param string $namespace
     * @throws Exception\InvalidArgumentException
     * @return void
     */
    
public function setNamespace($namespace)
    {
        if (empty(
$namespace)) {
            
$this->namespace '';
            return;
        }

        if (!
is_string($namespace) || !preg_match('/[a-z0-9_\.]+/i'$namespace)) {
            throw new 
Exception\InvalidArgumentException('Invalid namespace');
        }

        
$this->namespace $namespace;
    }

    
/**
     * Return method's namespace
     *
     * @return string
     */
    
public function getNamespace()
    {
        return 
$this->namespace;
    }

    
/**
     * Set the description
     *
     * @param string $string
     * @throws Exception\InvalidArgumentException
     * @return void
     */
    
public function setDescription($string)
    {
        if (!
is_string($string)) {
            throw new 
Exception\InvalidArgumentException('Invalid description');
        }

        
$this->description $string;
    }

    
/**
     * Retrieve the description
     *
     * @return string
     */
    
public function getDescription()
    {
        return 
$this->description;
    }

    
/**
     * Retrieve all prototypes as array of
     * {@link \Zend\Server\Reflection\Prototype}s
     *
     * @return Prototype[]
     */
    
public function getPrototypes()
    {
        return 
$this->prototypes;
    }

    
/**
     * Retrieve additional invocation arguments
     *
     * @return array
     */
    
public function getInvokeArguments()
    {
        return 
$this->argv;
    }

    
/**
     * Wakeup from serialization
     *
     * Reflection needs explicit instantiation to work correctly. Re-instantiate
     * reflection object on wakeup.
     *
     * @return void
     */
    
public function __wakeup()
    {
        if (
$this->reflection instanceof PhpReflectionMethod) {
            
$class = new PhpReflectionClass($this->class);
            
$this->reflection = new PhpReflectionMethod($class->newInstance(), $this->getName());
        } else {
            
$this->reflection = new PhpReflectionFunction($this->getName());
        }
    }
}