<?php
/**
 * Creates the methodTable for a service class.
 *
 * @usage $this->methodTable = MethodTable::create($this);
 * @author Christophe Herreman
 * @since 05/01/2005
 * @version $id$
 * 
 * Special contributions by Allessandro Crugnola and Ted Milker
 */

if (!defined('T_ML_COMMENT')) {
   define('T_ML_COMMENT', T_COMMENT);
} else {
   define('T_DOC_COMMENT', T_ML_COMMENT);
}

function strrstr($haystack, $needle)
{
	return substr($haystack, 0, strpos($haystack.$needle,$needle));
}

function strstrafter($haystack, $needle)
{
	return substr(strstr($haystack, $needle), strlen($needle));
}

class MethodTable
{
	/**
	 * Constructor.
	 *
	 * Since this class should only be accessed through the static create() method
	 * this constructor should be made private. Unfortunately, this is not possible
	 * in PHP4.
	 *
	 * @access private
	 */
	function MethodTable(){
	}


	/**
	 * Creates the methodTable for a passed class.
	 *
	 * @static
	 * @access public
	 * @param $className(String) The name of the service class.
	 *        May also simply be __FILE__
	 * @param $servicePath(String) The location of the classes (optional)
	 */
	function create($className, $servicePath = NULL, &$classComment){
		
		$methodTable = array();
		if(file_exists($className))
		{
			//The new __FILE__ way of doing things was used
			$sourcePath = $className;
			$className = str_replace("\\", '/', $className);
			$className = substr($className, strrpos($className, '/') + 1);
			$className = str_replace('.php', '', $className);
		}
		else
		{
			$className = str_replace('.php', '', $className);
			$fullPath = str_replace('.', '/', $className);
			$className = $fullPath;
			if(strpos($fullPath, '/') !== FALSE)
			{
				$className = substr(strrchr($fullPath, '/'), 1);
			}
			
			if($servicePath == NULL)
			{
				if(isset($GLOBALS['amfphp']['classPath']))
				{
					$servicePath = $GLOBALS['amfphp']['classPath'];
				}
				else
				{
					$servicePath = "../services/";
				}
			}
			$sourcePath = $servicePath . $fullPath . ".php";
		}
		
		if(!file_exists($sourcePath))
		{
			trigger_error("The MethodTable class could not find {" . 
				$sourcePath . "}", 
				E_USER_ERROR);
		}
		
		if(class_exists('ReflectionClass'))
		{
			//PHP5
			$classMethods = MethodTable::getClassMethodsReflection($sourcePath, $className, $classComment);
		}
		else
		{
			//PHP4
			
			$classMethods = MethodTable::getClassMethodsTokenizer($sourcePath, $className, $classComment);
		}
		
		foreach ($classMethods as $key => $value) {
			if($value['name'][0] == '_' || $value['name'] == 'beforeFilter')
			{
				continue;
			}
			$methodSignature = $value['args'];
			$methodName = $value['name'];
			$methodComment = $value['comment'];
			
			$description = MethodTable::getMethodDescription($methodComment) . " " . MethodTable::getMethodCommentAttribute($methodComment, "desc");
			$description = trim($description);
			$access = MethodTable::getMethodCommentAttributeFirstWord($methodComment, "access");
			$roles = MethodTable::getMethodCommentAttributeFirstWord($methodComment, "roles");
			$instance = MethodTable::getMethodCommentAttributeFirstWord($methodComment, "instance");
			$returns = MethodTable::getMethodCommentAttributeFirstLine($methodComment, "returns");
			$pagesize = MethodTable::getMethodCommentAttributeFirstWord($methodComment, "pagesize");
			$params = MethodTable::getMethodCommentArguments($methodComment);
						
			//description, arguments, access, [roles, [instance, [returns, [pagesize]]]]
			$methodTable[$methodName] = array();
			//$methodTable[$methodName]["signature"] = $methodSignature; //debug purposes
			$methodTable[$methodName]["description"] = ($description == "") ? "No description given." : $description;
			$methodTable[$methodName]["arguments"] = MethodTable::getMethodArguments($methodSignature, $params);
			$methodTable[$methodName]["access"] = ($access == "") ? "private" : $access;
			
			if($roles != "") $methodTable[$methodName]["roles"] = $roles;
			if($instance != "") $methodTable[$methodName]["instance"] = $instance;
			if($returns != "") $methodTable[$methodName]["returns"] = $returns;
			if($pagesize != "") $methodTable[$methodName]["pagesize"] = $pagesize;
		}
		
		$classComment = trim(str_replace("\r\n", "\n", MethodTable::getMethodDescription($classComment)));
		
		return $methodTable;
	}
	
	function getClassMethodsReflection($sourcePath, $className, & $classComment)
	{
		//Include the class in question
		$dir = dirname($sourcePath);
		if(!is_dir($dir))
		{
			return array();
		}
		
		chdir($dir);
		
		if(!file_exists($sourcePath))
		{
			return array();
		}
		
		//HACK: eAccelerator
		//Check if eAccelator is installed
		if( extension_loaded( "eAccelerator" ))
		{
			//Touch the file so the results of getDocComment will be accurate
			touch($sourcePath);
		}
		
		$included = include_once($sourcePath);
		if($included === FALSE)
		{
			return array();
		}
		
		//Verify that the class exists
		if(!class_exists($className))
		{
			return array();
		}
		
		$methodTable = array();
		
		$class = new ReflectionClass($className);
		
		$classComment = $class->getDocComment();
		$methods = $class->getMethods();
		
		
		foreach($methods as $reflectionMethod){
			
			if($reflectionMethod->isPublic() && $method->name[0] != '_' && $method->name != 'beforeFilter')
			{
				if($reflectionMethod->isConstructor())
				{
					$classComment .= $reflectionMethod->getDocComment();
				}
				else
				{
					$reflectionParameter = $reflectionMethod->getParameters();
					
					$methodTableEntry = array();			
					$parameters = array();
					
					foreach($reflectionParameter as $parameter){
						$parameters[] = $parameter->getName();
					}
					
					$methodTableEntry['args'] = '(' . implode(', ', $parameters);
					$methodTableEntry['name'] = $reflectionMethod->name;
					$methodTableEntry['comment'] = $reflectionMethod->getDocComment();
					
					$methodTable[] = $methodTableEntry;
				}
			}
		}
		
		return $methodTable;
	}
	
	function getClassMethodsTokenizer($sourcePath, $className, & $classComment)
	{
		$source = file_get_contents($sourcePath);
		$tokens = token_get_all($source);
		
		$waitingForOpenParenthesis = false;
		$waitingForFunction = false;
		$waitingForClassName = false;
		$bufferingArgs = false;
		$argBuffer = "";
		$lastFunction = "";
		$lastFunctionComment = "";
		$lastComment = "";
		$classMethods = array();
		$realClassName = "";

		$openBraces = -10000;

		$waitingForEndEncapsedString = false;
		foreach($tokens as $token)
		{
		   if (is_string($token)) {
				if($token == '{')
				{
					$openBraces++;
				}
				if($token == '}')
				{
					if($waitingForEndEncapsedString)
					{
						$waitingForEndEncapsedString = false;
					}
					else
					{
						$lastComment = '';
						$openBraces--;
						
						if($openBraces == 0)
						{
							//break;
						}
					}
				}
				elseif($waitingForOpenParenthesis && $token == '(')
				{
					$bufferingArgs = true;
					$argBuffer = "";
					$waitingForOpenParenthesis = false;
				}
				elseif($bufferingArgs)
				{
					if($token != ')')
					{
						$argBuffer .= $token;
					}
					else
					{
						if($lastFunction != $realClassName && $lastFunction != "__construct")
						{
							$classMethods[] = array("name" => $lastFunction,
											   "comment" => $lastFunctionComment,
											   "args" => $argBuffer);
						}
						else
						{
							$classComment .= "\n\n" . $lastComment;
						}
						
						$bufferingArgs = false;
						$argBuffer = "";
						$lastFunction = "";
						$lastFunctionComment = "";
					}
					
				}
		   } else {
			   // token array
			   list($id, $text) = $token;
				
				if($bufferingArgs)
				{
					$argBuffer .= $text;                    
				}
			   switch ($id) 
			   {
					
				   case T_COMMENT:
				   case T_ML_COMMENT: // we've defined this
				   case T_DOC_COMMENT: // and this
				   // no action on comments
						$lastComment = $text;
						break;
				   case T_FUNCTION:
						if($openBraces >= 1)
						{
							$waitingForFunction = true;
						}
						break;
					case T_STRING:
						if($waitingForFunction)
						{
							$waitingForFunction = false;
							$waitingForOpenParenthesis = true;
							$lastFunction = $text;
							$lastFunctionComment = $lastComment;
							$lastComment = "";              
						}
						if($waitingForClassName)
						{
							$waitingForClassName = false;
							if(strpos(strtolower($className), strtolower($text)) !== FALSE)
							{
								//Not the class we were looking for
								$classComment = $lastComment;
								$realClassName = $text;
							}
						}
						break;
					case T_CLASS:
						$openBraces = 0;
						$waitingForClassName = true;
						break;
					case T_CURLY_OPEN:
					case T_DOLLAR_OPEN_CURLY_BRACES:
						$waitingForEndEncapsedString = true;
						break;
				}
			}
		}
		return $classMethods;
	}
	
	/**
	 * 
	 */
	function getMethodCommentArguments($comment)
	{
		$pieces = explode('@param', $comment);
		$args = array();
		if(is_array($pieces) && count($pieces) > 1)
		{
			for($i = 0; $i < count($pieces) - 1; $i++)
			{
				$ps = strrstr($pieces[$i + 1], '@');
				$ps = strrstr($ps, '*/');
				$args[] = MethodTable::cleanComment($ps);
			}
		}
		return $args;
	}
	
	
	/**
	 * Returns the description from the comment.
	 * The description is(are) the first line(s) in the comment.
	 *
	 * @static
	 * @private
	 * @param $comment(String) The method's comment.
	 */
	function getMethodDescription($comment){
		$comment = MethodTable::cleanComment(strrstr($comment, "@"));
		return trim($comment);
	}
	
	
	/**
	 * Returns the value of a comment attribute.
	 *
	 * @static
	 * @private
	 * @param $comment(String) The method's comment.
	 * @param $attribute(String) The name of the attribute to get its value from.
	 */
	function getMethodCommentAttribute($comment, $attribute){
		$pieces = strstrafter($comment, '@' . $attribute);
		if($pieces !== FALSE)
		{
			$pieces = strrstr($pieces, '@');
			$pieces = strrstr($pieces, '*/');
			return MethodTable::cleanComment($pieces);
		}
		return "";
	}
	
	/**
	 * Returns the value of a comment attribute.
	 *
	 * @static
	 * @private
	 * @param $comment(String) The method's comment.
	 * @param $attribute(String) The name of the attribute to get its value from.
	 */
	function getMethodCommentAttributeFirstLine($comment, $attribute){
		$pieces = strstrafter($comment, '@' . $attribute);
		if($pieces !== FALSE)
		{
			$pieces = strrstr($pieces, '@');
			$pieces = strrstr($pieces, "*");
			$pieces = strrstr($pieces, "/");
			$pieces = strrstr($pieces, "-");
			$pieces = strrstr($pieces, "\n");
			$pieces = strrstr($pieces, "\r");
			$pieces = strrstr($pieces, '*/');
			return MethodTable::cleanComment($pieces);
		}
		return "";
	}
	
	function getMethodCommentAttributeFirstWord($comment, $attribute){
		$pieces = strstrafter($comment, '@' . $attribute);
		if($pieces !== FALSE)
		{
			$val = MethodTable::cleanComment($pieces);
			return trim(strrstr($val, ' '));
		}
		return "";
	}
	
	/**
	 * Returns an array with the arguments of a method.
	 *
	 * @static
	 * @access private
	 * @param $methodSignature (String)The method's signatureg;
	 */
	function getMethodArguments($methodSignature, $commentParams){
		if(strlen($methodSignature) < 2){
			//no arguments, return an empty array
			$result = array();
		}else{
			//clean the arguments before returning them
			$result = MethodTable::cleanArguments(explode(",", $methodSignature), $commentParams);
		}
		
		return $result;
	}
	
	
	/**
	 * Cleans the arguments array.
	 * This method removes all whitespaces and the leading "$" sign from each argument
	 * in the array.
	 *
	 * @static
	 * @access private
	 * @param $args(Array) The "dirty" array with arguments.
	 */
	function cleanArguments($args, $commentParams){
		$result = array();
		
		foreach($args as $index => $arg){
			$arg = strrstr(str_replace('(', '', $arg), '=');
			if(!isset($commentParams[$index]))
			{
				$result[] = trim($arg);
			}
			else
			{
				$start = trim($arg);
				$end = trim(str_replace('$', '', $commentParams[$index]));
				//echo($start);
				//echo($end);
				if($end != "" && $start != "" && strpos(strtolower($end), strtolower($start)) === 0)
				{
					$end = substr($end, strlen($start));
				}
				$result[] = $start . ' - ' . trim($end);
			}
		}
		
		return $result;
	}
	
	
	/**
	 * Cleans the comment string by removing all comment start and end characters.
	 *
	 * @static
	 * @private
	 * @param $comment(String) The method's comment.
	 */
	function cleanComment($comment){
		$comment = str_replace("/**", "", $comment);
		$comment = str_replace("*/", "", $comment);
		$comment = str_replace("*", "", $comment);
		$comment = str_replace("\r", "", trim($comment));
		$comment = eregi_replace("\n[ \t]+", "\n", trim($comment));
		$comment = str_replace("\n", "\\n", trim($comment));
		$comment = eregi_replace("[\t ]+", " ", trim($comment));
		
		$comment = str_replace("\"", "\\\"", $comment);
		return $comment;
	}

	/**
	 *
	 */
	function showCode($methodTable){
		

		foreach($methodTable as $methodName=>$methodProps){
			$result .= "\n\t\"" . $methodName . "\" => array(";
			
			foreach($methodProps as $key=>$value){
				$result .= "\n\t\t\"" . $key . "\" => ";

				if($key=="arguments"){
					$result .= "array(";
					for($i=0; $i<count($value); $i++){
						$result .= "\"" . addslashes($value[$i]) . "\"";
						if($i<count($value)-1){
							$result .= ", ";
						}
					}
					$result .= ")";
				}else{
					$result .= "\"" . $value . "\"";
				}

				$result .= ",";
			}
			
			$result = substr($result, 0, -1);
			$result .= "\n\t),";
		}
		
		$result = substr($result, 0, -1);
		$result = "\$this->methodTable = array(" . $result;
		$result .= "\n);";
			
		return $result;
	}
}
?>