<?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;
use 
Zend\Code\Exception;
use 
Zend\Code\NameInformation;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
/**
     * @param  array $classTokens
     * @param  NameInformation|null $nameInformation
     * @return ClassScanner
     */
    
public function __construct(array $classTokensNameInformation $nameInformation null)
    {
        
$this->tokens          $classTokens;
        
$this->nameInformation $nameInformation;
    }

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

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

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

        return 
$this->docComment;
    }

    
/**
     * @return false|DocBlockScanner
     */
    
public function getDocBlock()
    {
        if (!
$docComment $this->getDocComment()) {
            return 
false;
        }

        return new 
DocBlockScanner($docComment);
    }

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

    
/**
     * @return null|string
     */
    
public function getShortName()
    {
        
$this->scan();
        return 
$this->shortName;
    }

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

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

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

    
/**
     * @return bool
     */
    
public function isInstantiable()
    {
        
$this->scan();
        return (!
$this->isAbstract && !$this->isInterface);
    }

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

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

    
/**
     * @return bool
     */
    
public function hasParentClass()
    {
        
$this->scan();
        return (
$this->parentClass != null);
    }

    
/**
     * @return null|string
     */
    
public function getParentClass()
    {
        
$this->scan();
        return 
$this->parentClass;
    }

    
/**
     * @return array
     */
    
public function getInterfaces()
    {
        
$this->scan();
        return 
$this->interfaces;
    }

    
/**
     * @return array
     */
    
public function getConstants()
    {
        
$this->scan();

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

            
$return[] = $info['name'];
        }

        return 
$return;
    }

    
/**
     * Returns a list of property names
     *
     * @return array
     */
    
public function getPropertyNames()
    {
        
$this->scan();

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

            
$return[] = $info['name'];
        }

        return 
$return;
    }

    
/**
     * Returns a list of properties
     *
     * @return array
     */
    
public function getProperties()
    {
        
$this->scan();

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

            
$return[] = $this->getProperty($info['name']);
        }

        return 
$return;
    }

    public function 
getProperty($propertyNameOrInfoIndex)
    {
        
$this->scan();

        if (
is_int($propertyNameOrInfoIndex)) {
            
$info $this->infos[$propertyNameOrInfoIndex];
            if (
$info['type'] != 'property') {
                throw new 
Exception\InvalidArgumentException('Index of info offset is not about a property');
            }
        } elseif (
is_string($propertyNameOrInfoIndex)) {
            
$propertyFound false;
            foreach (
$this->infos as $info) {
                if (
$info['type'] === 'property' && $info['name'] === $propertyNameOrInfoIndex) {
                    
$propertyFound true;
                    break;
                }
            }
            if (!
$propertyFound) {
                return 
false;
            }
        } else {
            throw new 
Exception\InvalidArgumentException('Invalid property name of info index type.  Must be of type int or string');
        }
        if (!isset(
$info)) {
            return 
false;
        }
        
$p = new PropertyScanner(
            
array_slice($this->tokens$info['tokenStart'], $info['tokenEnd'] - $info['tokenStart'] + 1),
            
$this->nameInformation
        
);
        
$p->setClass($this->name);
        
$p->setScannerClass($this);
        return 
$p;
    }

    
/**
     * @return array
     */
    
public function getMethodNames()
    {
        
$this->scan();

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

            
$return[] = $info['name'];
        }

        return 
$return;
    }

    
/**
     * @return MethodScanner[]
     */
    
public function getMethods()
    {
        
$this->scan();

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

            
$return[] = $this->getMethod($info['name']);
        }

        return 
$return;
    }

    
/**
     * @param  string|int $methodNameOrInfoIndex
     * @throws Exception\InvalidArgumentException
     * @return MethodScanner
     */
    
public function getMethod($methodNameOrInfoIndex)
    {
        
$this->scan();

        if (
is_int($methodNameOrInfoIndex)) {
            
$info $this->infos[$methodNameOrInfoIndex];
            if (
$info['type'] != 'method') {
                throw new 
Exception\InvalidArgumentException('Index of info offset is not about a method');
            }
        } elseif (
is_string($methodNameOrInfoIndex)) {
            
$methodFound false;
            foreach (
$this->infos as $info) {
                if (
$info['type'] === 'method' && $info['name'] === $methodNameOrInfoIndex) {
                    
$methodFound true;
                    break;
                }
            }
            if (!
$methodFound) {
                return 
false;
            }
        }
        if (!isset(
$info)) {
            
// @todo find a way to test this
            
die('Massive Failure, test this');
        }
        
$m = new MethodScanner(
            
array_slice($this->tokens$info['tokenStart'], $info['tokenEnd'] - $info['tokenStart'] + 1),
            
$this->nameInformation
        
);
        
$m->setClass($this->name);
        
$m->setScannerClass($this);

        return 
$m;
    }

    
/**
     * @param  string $name
     * @return bool
     */
    
public function hasMethod($name)
    {
        
$this->scan();

        foreach (
$this->infos as $info) {
            if (
$info['type'] === 'method' && $info['name'] === $name) {
                return 
true;
            }
        }

        return 
false;
    }

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

    public function 
__toString()
    {
        
// @todo
    
}

    
/**
     * @return void
     * @throws Exception\RuntimeException
     */
    
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;
        
$namespace    null;
        
$infoIndex    0;
        
$braceCount   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 {
                
$lastTokenArray $token;
                list(
$tokenType$tokenContent$tokenLine) = $token;
            }

            return 
$tokenIndex;
        };
        
$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:

        switch (
$tokenType) {

            case 
T_DOC_COMMENT:

                
$this->docComment $tokenContent;
                goto 
SCANNER_CONTINUE;

            case 
T_FINAL:
            case 
T_ABSTRACT:
            case 
T_CLASS:
            case 
T_INTERFACE:

                
// CLASS INFORMATION

                
$classContext        null;
                
$classInterfaceIndex 0;

                
SCANNER_CLASS_INFO_TOP:

                if (
is_string($tokens[$tokenIndex 1]) && $tokens[$tokenIndex 1] === '{') {
                    goto 
SCANNER_CLASS_INFO_END;
                }

                
$this->lineStart $tokenLine;

                switch (
$tokenType) {

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

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

                    case 
T_INTERFACE:
                        
$this->isInterface true;
                    case 
T_CLASS:
                        
$this->shortName $tokens[$tokenIndex 2][1];
                        if (
$this->nameInformation && $this->nameInformation->hasNamespace()) {
                            
$this->name $this->nameInformation->getNamespace() . '\\' $this->shortName;
                        } else {
                            
$this->name $this->shortName;
                        }
                        goto 
SCANNER_CLASS_INFO_CONTINUE;

                    case 
T_NS_SEPARATOR:
                    case 
T_STRING:
                        switch (
$classContext) {
                            case 
T_EXTENDS:
                                
$this->shortParentClass .= $tokenContent;
                                break;
                            case 
T_IMPLEMENTS:
                                
$this->shortInterfaces[$classInterfaceIndex] .= $tokenContent;
                                break;
                        }
                        goto 
SCANNER_CLASS_INFO_CONTINUE;

                    case 
T_EXTENDS:
                    case 
T_IMPLEMENTS:
                        
$classContext $tokenType;
                        if ((
$this->isInterface && $classContext === T_EXTENDS) || $classContext === T_IMPLEMENTS) {
                            
$this->shortInterfaces[$classInterfaceIndex] = '';
                        } elseif (!
$this->isInterface && $classContext === T_EXTENDS) {
                            
$this->shortParentClass '';
                        }
                        goto 
SCANNER_CLASS_INFO_CONTINUE;

                    case 
null:
                        if (
$classContext == T_IMPLEMENTS && $tokenContent == ',') {
                            
$classInterfaceIndex++;
                            
$this->shortInterfaces[$classInterfaceIndex] = '';
                        }

                }

                
SCANNER_CLASS_INFO_CONTINUE:

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

                
SCANNER_CLASS_INFO_END:

                goto 
SCANNER_CONTINUE;

        }

        if (
$tokenType === null && $tokenContent === '{' && $braceCount === 0) {

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

            
SCANNER_CLASS_BODY_TOP:

            if (
$braceCount === 0) {
                goto 
SCANNER_CLASS_BODY_END;
            }

            switch (
$tokenType) {

                case 
T_CONST:

                    
$infos[$infoIndex] = array(
                        
'type'          => 'constant',
                        
'tokenStart'    => $tokenIndex,
                        
'tokenEnd'      => null,
                        
'lineStart'     => $tokenLine,
                        
'lineEnd'       => null,
                        
'name'          => null,
                        
'value'         => null,
                    );

                    
SCANNER_CLASS_BODY_CONST_TOP:

                    if (
$tokenContent === ';') {
                        goto 
SCANNER_CLASS_BODY_CONST_END;
                    }

                    if (
$tokenType === T_STRING) {
                        
$infos[$infoIndex]['name'] = $tokenContent;
                    }

                    
SCANNER_CLASS_BODY_CONST_CONTINUE:

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

                    
SCANNER_CLASS_BODY_CONST_END:

                    
$MACRO_INFO_ADVANCE();
                    goto 
SCANNER_CLASS_BODY_CONTINUE;

                case 
T_DOC_COMMENT:
                case 
T_PUBLIC:
                case 
T_PROTECTED:
                case 
T_PRIVATE:
                case 
T_ABSTRACT:
                case 
T_FINAL:
                case 
T_VAR:
                case 
T_FUNCTION:

                    
$infos[$infoIndex] = array(
                        
'type'        => null,
                        
'tokenStart'  => $tokenIndex,
                        
'tokenEnd'    => null,
                        
'lineStart'   => $tokenLine,
                        
'lineEnd'     => null,
                        
'name'        => null,
                    );

                    
$memberContext     null;
                    
$methodBodyStarted false;

                    
SCANNER_CLASS_BODY_MEMBER_TOP:

                    if (
$memberContext === 'method') {
                        switch (
$tokenContent) {
                            case 
'{':
                                
$methodBodyStarted true;
                                
$braceCount++;
                                goto 
SCANNER_CLASS_BODY_MEMBER_CONTINUE;
                            case 
'}':
                                
$braceCount--;
                                goto 
SCANNER_CLASS_BODY_MEMBER_CONTINUE;
                        }
                    }

                    if (
$memberContext !== null) {
                        if (
                            (
$memberContext === 'property' && $tokenContent === ';')
                            || (
$memberContext === 'method' && $methodBodyStarted && $braceCount === 1)
                            || (
$memberContext === 'method' && $this->isInterface && $tokenContent === ';')
                        ) {
                            goto 
SCANNER_CLASS_BODY_MEMBER_END;
                        }
                    }

                    switch (
$tokenType) {

                        case 
T_VARIABLE:
                            if (
$memberContext === null) {
                                
$memberContext             'property';
                                
$infos[$infoIndex]['type'] = 'property';
                                
$infos[$infoIndex]['name'] = ltrim($tokenContent'$');
                            }
                            goto 
SCANNER_CLASS_BODY_MEMBER_CONTINUE;

                        case 
T_FUNCTION:
                            
$memberContext             'method';
                            
$infos[$infoIndex]['type'] = 'method';
                            goto 
SCANNER_CLASS_BODY_MEMBER_CONTINUE;

                        case 
T_STRING:
                            if (
$memberContext === 'method' && $infos[$infoIndex]['name'] === null) {
                                
$infos[$infoIndex]['name'] = $tokenContent;
                            }
                            goto 
SCANNER_CLASS_BODY_MEMBER_CONTINUE;
                    }

                    
SCANNER_CLASS_BODY_MEMBER_CONTINUE:

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

                    
SCANNER_CLASS_BODY_MEMBER_END:

                    
$memberContext null;
                    
$MACRO_INFO_ADVANCE();
                    goto 
SCANNER_CLASS_BODY_CONTINUE;

                case 
null// no type, is a string

                    
switch ($tokenContent) {
                        case 
'{':
                            
$braceCount++;
                            goto 
SCANNER_CLASS_BODY_CONTINUE;
                        case 
'}':
                            
$braceCount--;
                            goto 
SCANNER_CLASS_BODY_CONTINUE;
                    }
            }

            
SCANNER_CLASS_BODY_CONTINUE:

            if (
$braceCount === || $MACRO_TOKEN_ADVANCE() === false) {
                goto 
SCANNER_CONTINUE;
            }
            goto 
SCANNER_CLASS_BODY_TOP;

            
SCANNER_CLASS_BODY_END:

            goto 
SCANNER_CONTINUE;

        }

        
SCANNER_CONTINUE:

        if (
$tokenContent === '}') {
            
$this->lineEnd $tokenLine;
        }

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

        
SCANNER_END:

        
// process short names
        
if ($this->nameInformation) {
            if (
$this->shortParentClass) {
                
$this->parentClass $this->nameInformation->resolveName($this->shortParentClass);
            }
            if (
$this->shortInterfaces) {
                foreach (
$this->shortInterfaces as $siIndex => $si) {
                    
$this->interfaces[$siIndex] = $this->nameInformation->resolveName($si);
                }
            }
        } else {
            
$this->parentClass $this->shortParentClass;
            
$this->interfaces  $this->shortInterfaces;
        }

        
$this->isScanned true;

        return;
    }
}