<?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\Code\Scanner;

use 
Zend\Code\Annotation\AnnotationManager;
use 
Zend\Code\Exception;
use 
Zend\Code\NameInformation;

class 
MethodScanner implements ScannerInterface
{
    
/**
     * @var bool
     */
    
protected $isScanned    false;

    
/**
     * @var string
     */
    
protected $docComment   null;

    
/**
     * @var ClassScanner
     */
    
protected $scannerClass null;

    
/**
     * @var string
     */
    
protected $class        null;

    
/**
     * @var string
     */
    
protected $name         null;

    
/**
     * @var int
     */
    
protected $lineStart    null;

    
/**
     * @var int
     */
    
protected $lineEnd      null;

    
/**
     * @var bool
     */
    
protected $isFinal false;

    
/**
     * @var bool
     */
    
protected $isAbstract false;

    
/**
     * @var bool
     */
    
protected $isPublic true;

    
/**
     * @var bool
     */
    
protected $isProtected false;

    
/**
     * @var bool
     */
    
protected $isPrivate false;

    
/**
     * @var bool
     */
    
protected $isStatic false;

    
/**
     * @var string
     */
    
protected $body '';

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

    
/**
     * @var NameInformation
     */
    
protected $nameInformation null;

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

    
/**
     * @param  array $methodTokens
     * @param NameInformation $nameInformation
     */
    
public function __construct(array $methodTokensNameInformation $nameInformation null)
    {
        
$this->tokens          $methodTokens;
        
$this->nameInformation $nameInformation;
    }

    
/**
     * @param  string $class
     * @return MethodScanner
     */
    
public function setClass($class)
    {
        
$this->class = (string) $class;
        return 
$this;
    }

    
/**
     * @param  ClassScanner  $scannerClass
     * @return MethodScanner
     */
    
public function setScannerClass(ClassScanner $scannerClass)
    {
        
$this->scannerClass $scannerClass;
        return 
$this;
    }

    
/**
     * @return MethodScanner
     */
    
public function getClassScanner()
    {
        return 
$this->scannerClass;
    }

    
/**
     * @return string
     */
    
public function getName()
    {
        
$this->scan();

        return 
$this->name;
    }

    
/**
     * @return int
     */
    
public function getLineStart()
    {
        
$this->scan();

        return 
$this->lineStart;
    }

    
/**
     * @return int
     */
    
public function getLineEnd()
    {
        
$this->scan();

        return 
$this->lineEnd;
    }

    
/**
     * @return string
     */
    
public function getDocComment()
    {
        
$this->scan();

        return 
$this->docComment;
    }

    
/**
     * @param  AnnotationManager $annotationManager
     * @return AnnotationScanner
     */
    
public function getAnnotations(AnnotationManager $annotationManager)
    {
        if ((
$docComment $this->getDocComment()) == '') {
            return 
false;
        }

        return new 
AnnotationScanner($annotationManager$docComment$this->nameInformation);
    }

    
/**
     * @return bool
     */
    
public function isFinal()
    {
        
$this->scan();

        return 
$this->isFinal;
    }

    
/**
     * @return bool
     */
    
public function isAbstract()
    {
        
$this->scan();

        return 
$this->isAbstract;
    }

    
/**
     * @return bool
     */
    
public function isPublic()
    {
        
$this->scan();

        return 
$this->isPublic;
    }

    
/**
     * @return bool
     */
    
public function isProtected()
    {
        
$this->scan();

        return 
$this->isProtected;
    }

    
/**
     * @return bool
     */
    
public function isPrivate()
    {
        
$this->scan();

        return 
$this->isPrivate;
    }

    
/**
     * @return bool
     */
    
public function isStatic()
    {
        
$this->scan();

        return 
$this->isStatic;
    }

    
/**
     * @return int
     */
    
public function getNumberOfParameters()
    {
        return 
count($this->getParameters());
    }

    
/**
     * @param  bool $returnScanner
     * @return array
     */
    
public function getParameters($returnScanner false)
    {
        
$this->scan();

        
$return = array();

        foreach (
$this->infos as $info) {
            if (
$info['type'] != 'parameter') {
                continue;
            }

            if (!
$returnScanner) {
                
$return[] = $info['name'];
            } else {
                
$return[] = $this->getParameter($info['name']);
            }
        }

        return 
$return;
    }

    
/**
     * @param  int|string $parameterNameOrInfoIndex
     * @return ParameterScanner
     * @throws Exception\InvalidArgumentException
     */
    
public function getParameter($parameterNameOrInfoIndex)
    {
        
$this->scan();

        if (
is_int($parameterNameOrInfoIndex)) {
            
$info $this->infos[$parameterNameOrInfoIndex];
            if (
$info['type'] != 'parameter') {
                throw new 
Exception\InvalidArgumentException('Index of info offset is not about a parameter');
            }
        } elseif (
is_string($parameterNameOrInfoIndex)) {
            foreach (
$this->infos as $info) {
                if (
$info['type'] === 'parameter' && $info['name'] === $parameterNameOrInfoIndex) {
                    break;
                }
                unset(
$info);
            }
            if (!isset(
$info)) {
                throw new 
Exception\InvalidArgumentException('Index of info offset is not about a parameter');
            }
        }

        
$p = new ParameterScanner(
            
array_slice($this->tokens$info['tokenStart'], $info['tokenEnd'] - $info['tokenStart']),
            
$this->nameInformation
        
);
        
$p->setDeclaringFunction($this->name);
        
$p->setDeclaringScannerFunction($this);
        
$p->setDeclaringClass($this->class);
        
$p->setDeclaringScannerClass($this->scannerClass);
        
$p->setPosition($info['position']);

        return 
$p;
    }

    
/**
     * @return string
     */
    
public function getBody()
    {
        
$this->scan();

        return 
$this->body;
    }

    public static function 
export()
    {
        
// @todo
    
}

    public function 
__toString()
    {
        
$this->scan();

        return 
var_export($thistrue);
    }

    protected function 
scan()
    {
        if (
$this->isScanned) {
            return;
        }

        if (!
$this->tokens) {
            throw new 
Exception\RuntimeException('No tokens were provided');
        }

        
/**
         * Variables & Setup
         */

        
$tokens       = &$this->tokens// localize
        
$infos        = &$this->infos// localize
        
$tokenIndex   null;
        
$token        null;
        
$tokenType    null;
        
$tokenContent null;
        
$tokenLine    null;
        
$infoIndex    0;
        
$parentCount  0;

        
/*
         * MACRO creation
         */
        
$MACRO_TOKEN_ADVANCE = function () use (&$tokens, &$tokenIndex, &$token, &$tokenType, &$tokenContent, &$tokenLine) {
            static 
$lastTokenArray null;
            
$tokenIndex = ($tokenIndex === null) ? $tokenIndex 1;
            if (!isset(
$tokens[$tokenIndex])) {
                
$token        false;
                
$tokenContent false;
                
$tokenType    false;
                
$tokenLine    false;

                return 
false;
            }
            
$token $tokens[$tokenIndex];
            if (
is_string($token)) {
                
$tokenType    null;
                
$tokenContent $token;
                
$tokenLine    $tokenLine substr_count($lastTokenArray[1],
                                                          
"\n"); // adjust token line by last known newline count
            
} else {
                list(
$tokenType$tokenContent$tokenLine) = $token;
            }

            return 
$tokenIndex;
        };
        
$MACRO_INFO_START    = function () use (&$infoIndex, &$infos, &$tokenIndex, &$tokenLine) {
            
$infos[$infoIndex] = array(
                
'type'        => 'parameter',
                
'tokenStart'  => $tokenIndex,
                
'tokenEnd'    => null,
                
'lineStart'   => $tokenLine,
                
'lineEnd'     => $tokenLine,
                
'name'        => null,
                
'position'    => $infoIndex 1// position is +1 of infoIndex
            
);
        };
        
$MACRO_INFO_ADVANCE  = function () use (&$infoIndex, &$infos, &$tokenIndex, &$tokenLine) {
            
$infos[$infoIndex]['tokenEnd'] = $tokenIndex;
            
$infos[$infoIndex]['lineEnd']  = $tokenLine;
            
$infoIndex++;

            return 
$infoIndex;
        };

        
/**
         * START FINITE STATE MACHINE FOR SCANNING TOKENS
         */

        // Initialize token
        
$MACRO_TOKEN_ADVANCE();

        
SCANNER_TOP:

        
$this->lineStart = ($this->lineStart) ? : $tokenLine;

        switch (
$tokenType) {
            case 
T_DOC_COMMENT:
                
$this->lineStart null;
                if (
$this->docComment === null && $this->name === null) {
                    
$this->docComment $tokenContent;
                }
                goto 
SCANNER_CONTINUE_SIGNATURE;

            case 
T_FINAL:
                
$this->isFinal true;
                goto 
SCANNER_CONTINUE_SIGNATURE;

            case 
T_ABSTRACT:
                
$this->isAbstract true;
                goto 
SCANNER_CONTINUE_SIGNATURE;

            case 
T_PUBLIC:
                
// use defaults
                
goto SCANNER_CONTINUE_SIGNATURE;

            case 
T_PROTECTED:
                
$this->isProtected true;
                
$this->isPublic    false;
                goto 
SCANNER_CONTINUE_SIGNATURE;

            case 
T_PRIVATE:
                
$this->isPrivate true;
                
$this->isPublic  false;
                goto 
SCANNER_CONTINUE_SIGNATURE;

            case 
T_STATIC:
                
$this->isStatic true;
                goto 
SCANNER_CONTINUE_SIGNATURE;

            case 
T_VARIABLE:
            case 
T_STRING:

                if (
$tokenType === T_STRING && $parentCount === 0) {
                    
$this->name $tokenContent;
                }

                if (
$parentCount === 1) {
                    if (!isset(
$infos[$infoIndex])) {
                        
$MACRO_INFO_START();
                    }
                    if (
$tokenType === T_VARIABLE) {
                        
$infos[$infoIndex]['name'] = ltrim($tokenContent'$');
                    }
                }

                goto 
SCANNER_CONTINUE_SIGNATURE;

            case 
null:

                switch (
$tokenContent) {
                    case 
'&':
                        if (!isset(
$infos[$infoIndex])) {
                            
$MACRO_INFO_START();
                        }
                        goto 
SCANNER_CONTINUE_SIGNATURE;
                    case 
'(':
                        
$parentCount++;
                        goto 
SCANNER_CONTINUE_SIGNATURE;
                    case 
')':
                        
$parentCount--;
                        if (
$parentCount === 0) {
                            if (
$infos) {
                                
$MACRO_INFO_ADVANCE();
                            }
                            
$context 'body';
                        }
                        goto 
SCANNER_CONTINUE_BODY;
                    case 
',':
                        if (
$parentCount === 1) {
                            
$MACRO_INFO_ADVANCE();
                        }
                        goto 
SCANNER_CONTINUE_SIGNATURE;
                }
        }

        
SCANNER_CONTINUE_SIGNATURE:

        if (
$MACRO_TOKEN_ADVANCE() === false) {
            goto 
SCANNER_END;
        }
        goto 
SCANNER_TOP;

        
SCANNER_CONTINUE_BODY:

        
$braceCount 0;
        while (
$MACRO_TOKEN_ADVANCE() !== false) {
            if (
$tokenContent == '}') {
                
$braceCount--;
            }
            if (
$braceCount 0) {
                
$this->body .= $tokenContent;
            }
            if (
$tokenContent == '{') {
                
$braceCount++;
            }
            
$this->lineEnd $tokenLine;
        }

        
SCANNER_END:

        
$this->isScanned true;

        return;
    }
}