<?php

/**
 * DocBlock Generator
 *
 * PHP version 5
 *
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 * + Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 * + Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation and/or
 * other materials provided with the distribution.
 * + The names of its contributors may not be used to endorse or
 * promote products derived from this software without specific prior written permission.
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @category  PHP
 * @package   PHP_DocBlockGenerator
 * @author    Michel Corne <mcorne@yahoo.com>
 * @copyright 2007 Michel Corne
 * @license   http://www.opensource.org/licenses/bsd-license.php The BSD License
 * @version   SVN: $Id$
 * @link      http://pear.php.net/package/PHP_DocBlockGenerator
 */

require_once 'PHP/DocBlockGenerator/Block.php';
require_once 'PHP/CompatInfo.php';

/**
 * Extraction of the PHP objects/tokens of the source code and creation of the DocBlocks
 *
 * @category  PHP
 * @package   PHP_DocBlockGenerator
 * @author    Michel Corne <mcorne@yahoo.com>
 * @copyright 2007 Michel Corne
 * @license   http://www.opensource.org/licenses/bsd-license.php The BSD License
 * @version   SVN: $Id$
 * @link      http://pear.php.net/package/PHP_DocBlockGenerator
 */

class PHP_DocBlockGenerator_Tokens
{
    /**
     * The PHP_DocBlockGenerator_Block instance
     *
     * @var    object
     * @access private
     */
    private $block;

    /**
     * The source code End-Of-Line character
     *
     * EOL = "\r\n" for DOS/Windows, "\r" for MAC, "\n" for Unix
     *
     * @var    string
     * @access public
     */
    public $eol;

    /**
     * Flag indicating if the current token has a DocBlock or not
     *
     * @var    boolean
     * @access public
     */
    public $hasBlock;

    /**
     * The current token information,  including the token type, access type...
     *
     * @var    array
     * @access public
     */
    public $id;

    /**
     * Flag indicating if the current token is within a class
     *
     * @var    integer
     * @access public
     */
    public $inClass;

    /**
     * The none relevant PHP code tokens
     *
     * @var    array
     * @access private
     */
    private $noPHPCode = array(// /
        T_WHITESPACE, T_ENCAPSED_AND_WHITESPACE, // spaces
        T_DOC_COMMENT, T_COMMENT, // comments
        T_INLINE_HTML, // none PHP code
        );

    /**
     * Source code tokens excluding none relevant PHP code tokens
     *
     * @var    array
     * @access private
     */
    private $phpTokens = array();

    /**
     * The file PHP version
     *
     * @var    string
     * @access public
     */
    public $phpVersion = '';

    /**
     * Source code tokens
     *
     * @var    array
     * @access private
     */
    private $tokens = array();

    /**
     * The class constructor
     *
     * @return void
     * @access public
     */
    public function __construct()
    {
        $this->block = new PHP_DocBlockGenerator_Block($this);
        $this->info = new PHP_CompatInfo;
    }

    /**
     * Gets the current token
     *
     * @param  integer $id the token identification number
     * @return array   the token type and value, false if invalid ID
     * @access public
     */
    public function get($id)
    {
        return $this->isValid($id)? $this->tokens[$id] : false;
    }

    /**
     * Gets all the source code tokens and tidies them
     *
     * @param  string  $data the source code
     * @return boolean true on success, false on failure
     * @access private
     */
    private function getAll($data)
    {
        // extracts the tokens, tidies the tokens, extracts the PHP code only tokens
        $this->tokens = token_get_all($data) and array_walk($this->tokens, array($this, 'tidy')) and
        $this->phpTokens = array_filter($this->tokens, array($this, 'isPHPCode'));
        // /
        // dump of all the tokens for debugging purposes only
        // file_put_contents('tokens.txt', var_export($this->tokens, true));
        // /
        return (bool)$this->tokens;
    }

    /**
     * Determines the source code End Of Line character
     *
     * EOL = "\r\n" for DOS/Windows, "\r" for MAC, "\n" for Unix
     *
     * @access private
     */
    private function getEOL($data)
    {
        if (strpos($data, "\r\n") !== false) { // DOS/Windows EOL
            $this->eol = "\r\n";
        } else if (strpos($data, "\r") !== false) { // MAC EOL
            $this->eol = "\r";
        } else { // Unix EOL
            $this->eol = "\n";
        }
    }

    /**
     * Verifies that the token is a valid source code token
     *
     * @param  integer $id the token identification number
     * @return boolean true if valid, false otherwise
     * @access private
     */
    private function isValid($id)
    {
        return isset($this->tokens[$id]);
    }

    /**
     * Checks if the token is relevant PHP code, excluding spaces and comments.
     *
     * This is an array_filter() callback function.
     *
     * @param  array   $token the token
     * @return boolean true if relevant PHP code, false otherwise
     * @access private
     */
    private function isPHPCode($token)
    {
        return !in_array($token['type'], $this->noPHPCode);
    }

    /**
     * Processes the source code tokens
     *
     * Gets all the source code tokens. Determines the source code EOL.
     * Determines the file PHP version. Initializes the Page-level tags.
     * Parses the file tokens and creates their DocBlocks.
     * Re-assembles all the tokens with their DocBlocks.
     *
     * @param  string  $data  the source code
     * @param  array   $param the tags/parameters values
     * @return boolean true on success, false on failure
     * @access public
     */
    public function process($data, $param)
    {
        // get all the tokens
        if ($result = $this->getAll($data)) {
            // determines the source code EOL
            $this->getEOL($data);
            // extracts all the tokens
            $allTokens = $this->slice();
            // initializes the Page-level tags
            $this->block->init($param);

            $this->hasBlock = 0;
            $this->id = array();
            $this->inClass = null;
            $inFunct = null;
            $openTagID = null;
            $isPageBlock = false;

            foreach($this->tokens as $id => $token) {
                $value = $token['value'];

                switch ($type = $token['type']) {
                    case '{': // class or function opening curly brace
                    // note: none matching open and close curly braces will cause issues
                    case T_CURLY_OPEN:
                    case T_DOLLAR_OPEN_CURLY_BRACES: // ${
                        // counting braces within the function, and the class
                        is_null($inFunct) or $inFunct++;
                        is_null($this->inClass) or $this->inClass++;
                        break;

                    case '}': // class or function closing curly brace
                        // reached end of the function or class
                        is_null($inFunct) or --$inFunct or $inFunct = null;
                        is_null($this->inClass) or --$this->inClass or $this->inClass = null;
                        break;

                    case T_CLASS: // class classes and objects
                        $this->inClass = 0;
                        $this->block->setClass($id);
                        break;

                    case T_CONST: // const
                        // sets the const DocBlock
                        is_null($this->inClass) or $this->block->setConst($id);
                        break;

                    case T_CONSTANT_ENCAPSED_STRING: // "foo" or 'bar' string syntax
                        // sets the include DocBlock
                        isset($this->id[T_INCLUDE]) and $this->block->build($this->id[T_INCLUDE]);
                        break;

                    case T_DOC_COMMENT: // /**     */ PHPDoc style comments (PHP 5 only)
                        // spots the page block, realigns the DocBlock tags
                        $this->hasBlock = true;
                        $isPageBlock or $isPageBlock = (strpos($value, '@package') !== false);
                        $this->block->realign($id, $value);
                        break;

                    case T_FUNCTION: // function or cfunction functions
                        $inFunct = 0;
                        $this->block->setFunction($id); // sets function DocBlock
                        break;

                    case T_INCLUDE: // include() include()
                    case T_INCLUDE_ONCE: // include_once() include_once()
                    case T_REQUIRE: // require() require()
                    case T_REQUIRE_ONCE: // require_once() require_once()
                        $type = T_INCLUDE;
                    case T_GLOBAL: // global variable scope
                        // only capturing includes and globals outside of classes and functions
                        is_null($this->inClass) and is_null($inFunct) and $this->id[$type] = $id;
                        break;

                    case T_OPEN_TAG: // <?php, <? or <%
                        $openTagID = $id;
                        break;

                    case T_PRIVATE: // private classes and objects. PHP 5 only.
                    case T_PROTECTED: // protected classes and objects. PHP 5 only.
                    case T_PUBLIC: // public classes and objects. PHP 5 only.
                        // captures the class visibilty
                        is_null($this->inClass) or $this->id['access'] = array($id, $value);
                        break;

                    case T_STATIC: // static variable scope
                        if (!is_null($this->inClass) and is_null($inFunct)) {
                            // captures the static property within a class and outside of a function
                            // defaults the access to public
                            isset($this->id['access']) or $this->id['access'] = array($id, 'public');
                            // captures the static property inside a class and outside of a function
                            $this->id[$type] = $id;
                        }
                        break;

                    case T_VAR: // var classes and objects
                        // captures the class property
                        is_null($this->inClass) or $this->id[$type] = $id;
                        break;

                    case T_STRING:
                        // sets define DocBlock
                        $value == 'define' and $this->block->setDefine($id);
                        break;

                    case T_VARIABLE: // $foo variables
                        if (isset($this->id[T_INCLUDE])) {
                            // including a variable instead of a string, sets the include DocBlock
                            $this->block->build($this->id[T_INCLUDE]);
                        } else if (isset($this->id[T_GLOBAL])) {
                            // a global variable,  sets the global DocBlock, e.g. global $var
                            $this->block->setGlobal($this->id[T_GLOBAL], $token, $allTokens);
                        } else if (is_null($this->inClass) and is_null($inFunct) and $value == '$GLOBALS') {
                            // a GLOBALS variable outside of a class and function
                            // sets the global variable DocBlock, e.g. $GLOBALS['foo']
                            $this->block->setGLOBALS($id, $allTokens);
                        } else if (isset($this->id['access']) or isset($this->id[T_VAR])) {
                            // a class property, sets the class variable
                            $this->block->setVar($token);
                        }
                        break;
                }
            }
            if (!$isPageBlock) {
                // no Page-level DocBlock, determines the PHP version, sets the Page-level DocBlock
                $info = $this->info->parseString($data);
                list($this->phpVersion) = explode('.', $info['version']);
                $this->block->setPage($openTagID);
            }
            // re-assembles all the tokens
            $result = $this->putAll();
        }

        return $result;
    }

    /**
     * Re-assembles the source code tokens into a string
     *
     * @return boolean true on success, false on failure
     * @access private
     */
    private function putAll()
    {
        $callback = create_function('$string, $token', 'return $string .= $token[\'value\'];'); // creates the array_reduce callback
        return array_reduce($this->tokens, $callback); // reduces the array to a string made of token values
    }

    /**
     * Sets the token value
     *
     * @param  integer $id    the token identification number
     * @param  string  $value the token value
     * @return boolean true if the token is valid, false otherwise
     * @access public
     */
    public function set($id, $value)
    {
        $isValid = $this->isValid($id) and $this->tokens[$id]['value'] = $value;
        return $isValid;
    }

    /**
     * Slices a subset of tokens
     *
     * @param  integer $offset       the token identification number to start looking at
     * @param  string  $openBracket  the delimiter to start slicing at
     * @param  mixed   $closeBracket the delimiter to stop slicing at
     * @param  integer $bracketCount to set to 1 if the offset is past the first delimiter
     * @return array   the sliced tokens
     * @access private
     */
    public function slice($offset = 0, $openBracket = null, $closeBracket = null, $bracketCount = null)
    {
        $tokens = array();
        foreach($this->phpTokens as $token) {
            if ($token['id'] >= $offset) { // processes tokens after the offset
                $tokens[] = $token; // captures the token
                if ($openBracket !== null) { // only captures tokens delimited by the brackets
                    $openBracket == '{' and in_array($token['type'], array(T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES)) and
                    $token['type'] = '{'; // captures all types of open curly braces
                    $token['type'] == $openBracket and $bracketCount++ or // counts enclosed opening brackets
                    $token['type'] == $closeBracket and $bracketCount--; // counts enclosed closing brackets
                    if ($bracketCount === 0) { // reached the last closing bracket
                        break;
                    }
                }
            }
        }
        return $tokens;
    }

    /**
     * Tidies a token
     *
     * An array_walk() callback function.
     *
     * @param  array   &$token the token
     * @param  integer $id     the token identification number
     * @return void
     * @access private
     * @see    self::getAll()
     */
    private function tidy(&$token, $id)
    {
        if (is_array($token)) { // a PHP token
            $tidied['type'] = current($token); // extracts the token type, e.g. T_CONSTANT_ENCAPSED_STRING
            $tidied['type'] == T_PAAMAYIM_NEKUDOTAYIM and $tidied['type'] = T_DOUBLE_COLON;
            $tidied['value'] = next($token); // extracts the token value, e.g. "foo"
            $tidied['name'] = token_name($tidied['type']); // captures the token type as a string, note: only useful for debugging purposes
        } else { // a single character, e.g. ";"
            $tidied['type'] = $tidied['value'] = $token;
        }
        $tidied['id'] = $id; // captures the token position
        $token = $tidied;
    }
}

?>