<?php defined('SYSPATH') OR die('No direct access allowed.');
/**
 * Kohana - The Swift PHP Framework
 *
 *  License:
 *  author    - Kohana Team
 *  copyright - (c) 2007 Kohana Team
 *  license   - <http://kohanaphp.com/license.html>
 */

/**
 * Provides self-generating documentation about Kohana.
 */
class Kodoc_Core {

	protected static $types = array
	(
		'core',
		'config',
		'helpers',
		'libraries',
		'models',
		'views'
	);

	public static function get_types()
	{
		return self::$types;
	}

	public static function get_files()
	{
		// Extension length
		$ext_len = -(strlen(EXT));

		$files = array();
		foreach (self::$types as $type)
		{
			$files[$type] = array();
			foreach (Kohana::list_files($type, TRUE) as $file)
			{
				// Not a source file
				if (substr($file, $ext_len) !== EXT)
					continue;

				// Remove the dirs from the filename
				$file = preg_replace('!^.+'.$type.'/(.+)'.EXT.'$!', '$1', $file);

				// Skip utf8 function files
				if ($type === 'core' AND substr($file, 0, 5) === 'utf8/')
					continue;

				if ($type === 'libraries' AND substr($file, 0, 8) === 'drivers/')
				{
					// Remove the drivers directory from the file
					$file = explode('_', substr($file, 8));

					if (count($file) === 1)
					{
						// Driver interface
						$files[$type][current($file)][] = current($file);
					}
					else
					{
						// Driver is class suffix
						$driver = array_pop($file);

						// Library is everything else
						$library = implode('_', $file);

						// Library driver
						$files[$type][$library][] = $driver;
					}
				}
				else
				{
					$files[$type][$file] = NULL;
				}
			}
		}

		return $files;
	}

	public static function remove_docroot($file)
	{
		return preg_replace('!^'.preg_quote(DOCROOT, '!').'!', '', $file);
	}

	public static function humanize_type($types)
	{
		$types = is_array($types) ? $types : explode('|', $types);

		$output = array();
		while ($t = array_shift($types))
		{
			$output[] = '<tt>'.trim($t).'</tt>';
		}

		return implode(' or ', $output);
	}

	public static function humanize_value($value)
	{
		if ($value === NULL)
		{
			return 'NULL';
		}
		elseif (is_bool($value))
		{
			return $value ? 'TRUE' : 'FALSE';
		}
		elseif (is_string($value))
		{
			return 'string '.$value;
		}
		elseif (is_numeric($value))
		{
			return (is_int($value) ? 'int' : 'float').' '.$value;
		}
		elseif (is_array($value))
		{
			return 'array';
		}
		elseif (is_object($value))
		{
			return 'object '.get_class($value);
		}
	}

	// All files to be parsed
	protected $file = array();

	public function __construct($type, $filename)
	{
		// Parse the file
		$this->file = $this->parse($type, $filename);
	}

	/**
	 * Fetch documentation for all files parsed.
	 *
	 * Returns:
	 *  array: file documentation
	 */
	public function get()
	{
		return $this->file;
	}

	/**
	 * Parse a file for Kodoc commands, classes, and methods.
	 *
	 * Parameters:
	 *  string: file type
	 *  string: absolute filename path
	 */
	protected function parse($type, $filename)
	{
		// File definition
		$file = array
		(
			'type'      => $type,
			'comment'   => '',
			'file'      => self::remove_docroot($filename),
		);

		// Read the entire file into an array
		$data = file($filename);

		foreach ($data as $line)
		{
			if (strpos($line, 'class') !== FALSE AND preg_match('/(?:class|interface)\s+([a-z0-9_]+).+{$/i', $line, $matches))
			{
				// Include the file if it has not already been included
				class_exists($matches[1], FALSE) or include_once $filename;

				// Add class to file info
				$file['classes'][] = $this->parse_class($matches[1]);
			}
		}

		if (empty($file['classes']))
		{
			$block  = NULL;
			$source = NULL;

			foreach ($data as $line)
			{
				switch (substr(trim($line), 0, 2))
				{
					case '/*':
						$block = '';
						continue 2;
					break;
					case '*/':
						$source = TRUE;
						continue 2;
					break;
				}

				if ($source === TRUE)
				{
					if (preg_match('/\$config\[\'(.+?)\'\]\s+=\s+([^;].+)/', $line, $matches))
					{
						$source = array
						(
							$matches[1],
							$matches[2]
						);
					}
					else
					{
						$source = array();
					}

					$file['comments'][] = array_merge($this->parse_comment($block), array('source' => $source));

					$block  = NULL;
					$source = FALSE;
				}
				elseif (is_string($block))
				{
					$block .= $line;
				}
			}

		}

		return $file;
	}

	protected function parse_comment($block)
	{
		if (($block = trim($block)) == '')
			return $block;

		// Explode the lines into an array and trim them
		$block = array_map('trim', explode("\n", $block));

		if (current($block) === '/**')
		{
			// Remove comment opening
			array_shift($block);
		}

		if (end($block) === '*/')
		{
			// Remove comment closing
			array_pop($block);
		}

		// Start comment
		$comment = array();

		while ($line = array_shift($block))
		{
			// Remove * from the line
			$line = trim(substr($line, 2));

			if ($line[0] === '$' AND substr($line, -1) === '$')
			{
				// Skip SVN property inserts
				continue;
			}

			if ($line[0] === '@')
			{
				if (preg_match('/^@(.+?)\s+(.+)$/', $line, $matches))
				{
					$comment[$matches[1]][] = $matches[2];
				}
			}
			else
			{
				$comment['about'][] = $line;
			}
		}

		if ( ! empty($comment['about']))
		{
			$token = '';
			$block = '';
			$about = '';

			foreach ($comment['about'] as $line)
			{
				if (strpos($line, '`') !== FALSE)
				{
					$line = preg_replace('/`([^`].+?)`/', '<tt>$1</tt>', $line);
				}

				if (substr($line, 0, 2) === '- ')
				{
					if ($token !== 'ul')
					{
						$about .= $this->comment_block($token, $block);
						$block  = '';
					}

					$token = 'ul';
					$line  = '<li>'.trim(substr($line, 2)).'</li>'."\n";
				}
				elseif (preg_match('/(.+?)\s+-\s+(.+)/', $line, $matches))
				{
					if ($token !== 'dl')
					{
						$about .= $this->comment_block($token, $block);
						$block  = '';
					}

					$token = 'dl';
					$line = '<dt>'.$matches[1].'</dt>'."\n".'<dd>'.$matches[2].'</dd>'."\n";
				}
				else
				{
					$token = 'p';
					$line .= ' ';
				}

				if (trim($line) === '')
				{
					$about .= $this->comment_block($token, $block);
					$block = '';
				}
				else
				{
					$block .= $line;
				}
			}

			if ( ! empty($block))
			{
				$about .= $this->comment_block($token, $block);
			}

			$comment['about'] = $about;
		}

		return $comment;
	}

	protected function comment_block($token, $block)
	{
		if (empty($token) OR empty($block))
			return '';

		$block = trim($block);

		if ($block[0] === '<')
		{
			// Insert newlines before and after the block
			$block = "\n".$block."\n";
		}

		return '<'.$token.'>'.$block.'</'.$token.'>'."\n";
	}

	protected function parse_class($class)
	{
		// Use reflection to find information
		$reflection = new ReflectionClass($class);

		// Class definition
		$class = array
		(
			'name'       => $reflection->getName(),
			'comment'    => $this->parse_comment($reflection->getDocComment()),
			'final'      => $reflection->isFinal(),
			'abstract'   => $reflection->isAbstract(),
			'interface'  => $reflection->isInterface(),
			'extends'    => '',
			'implements' => array(),
			'methods'    => array()
		);

		if ($implements = $reflection->getInterfaces())
		{
			foreach ($implements as $interface)
			{
				// Get implemented interfaces
				$class['implements'][] = $interface->getName();
			}
		}

		if ($parent = $reflection->getParentClass())
		{
			// Get parent class
			$class['extends'] = $parent->getName();
		}

		if ($methods = $reflection->getMethods())
		{
			foreach ($methods as $method)
			{
				// Don't try to document internal methods
				if ($method->isInternal()) continue;

				$class['methods'][] = array
				(
					'name'       => $method->getName(),
					'comment'    => $this->parse_comment($method->getDocComment()),
					'class'      => $class['name'],
					'final'      => $method->isFinal(),
					'static'     => $method->isStatic(),
					'abstract'   => $method->isAbstract(),
					'visibility' => $this->visibility($method),
					'parameters' => $this->parameters($method)
				);
			}
		}

		return $class;
	}

	/**
	 * Finds the parameters for a ReflectionMethod.
	 *
	 * @param   object   ReflectionMethod
	 * @return  array
	 */
	protected function parameters(ReflectionMethod $method)
	{
		$params = array();

		if ($parameters = $method->getParameters())
		{
			foreach ($parameters as $param)
			{
				// Parameter data
				$data = array
				(
					'name' => $param->getName()
				);

				if ($param->isOptional())
				{
					// Set default value
					$data['default'] = $param->getDefaultValue();
				}

				$params[] = $data;
			}
		}

		return $params;
	}

	/**
	 * Finds the visibility of a ReflectionMethod.
	 *
	 * @param   object   ReflectionMethod
	 * @return  string
	 */
	protected function visibility(ReflectionMethod $method)
	{
		$vis = array_flip(Reflection::getModifierNames($method->getModifiers()));

		if (isset($vis['public']))
		{
			return 'public';
		}

		if (isset($vis['protected']))
		{
			return 'protected';
		}

		if (isset($vis['private']))
		{
			return 'private';
		}

		return FALSE;
	}

} // End Kodoc
class Kodoc_xCore {

	/**
	 * libraries, helpers, etc
	 */
	protected $files = array
	(
		'core'      => array(),
		'config'    => array(),
		'helpers'   => array(),
		'libraries' => array(),
		'models'    => array(),
		'views'     => array()
	);

	/**
	 * $classes[$name] = array $properties;
	 * $properties = array
	 * (
	 *     'drivers'    => array $drivers
	 *     'properties' => array $properties
	 *     'methods'    => array $methods
	 * )
	 */
	protected $classes = array();

	// Holds the current data until parsed
	protected $current_class;

	// $packages[$name] = array $files;
	protected $packages = array();

	// PHP's visibility types
	protected static $php_visibility = array
	(
		'public',
		'protected',
		'private'
	);

	public function __construct()
	{
		if (isset(self::$php_visibility[0]))
		{
			self::$php_visibility = array_flip(self::$php_visibility);
		}

		foreach ($this->files as $type => $files)
		{
			foreach (Kohana::list_files($type) as $filepath)
			{
				// Get the filename with no extension
				$file = pathinfo($filepath, PATHINFO_FILENAME);

				// Skip indexes and drivers
				if ($file === 'index' OR strpos($filepath, 'libraries/drivers') !== FALSE)
					continue;

				// Add the file
				$this->files[$type][$file] = $filepath;

				// Parse the file
				$this->parse_file($filepath);
			}
		}

		Kohana::log('debug', 'Kodoc Library initialized');
	}

	public function get_docs($format = 'html')
	{
		switch ($format)
		{
			default:
				// Generate HTML via a View
				$docs = new View('kodoc_html');

				$docs->set('classes', $this->classes)->render();
			break;
		}

		return $docs;
	}

	protected function parse_file($file)
	{
		$file = fopen($file, 'r');

		$i = 1;
		while ($line = fgets($file))
		{
			if (substr(trim($line), 0, 2) === '/*')
			{
				// Reset vars
				unset($current_doc, $section, $p);

				// Prepare for a new doc section
				$current_doc = array();
				$closing_tag = '*/';

				$current_block = 'description';
				$p = 0;

				// Assign the current doc
				$this->current_doc =& $current_doc;
			}
			elseif (isset($closing_tag))
			{
				if (substr(trim($line), 0, 1) === '*')
				{
					// Remove the leading comment
					$line = substr(ltrim($line), 2);

					if (preg_match('/^([a-z ]+):/i', $line, $matches))
					{
						$current_block = trim($matches[1]);
					}
					elseif (isset($current_doc))
					{
						$line = ltrim($line);

						if (preg_match('/^\-\s+(.+)/', $line, $matches))
						{
							// An unordered list
							$current_doc['html'][$current_block]['ul'][] = $matches[1];
						}
						elseif (preg_match('/^[0-9]+\.\s+(.+)/', $line, $matches))
						{
							// An ordered list
							$current_doc['html'][$current_block]['ol'][] = $matches[1];
						}
						elseif (preg_match('/^([a-zA-Z ]+)\s+\-\s+(.+)/', $line, $matches))
						{
							// Definition list
							$current_doc['html'][$current_block]['dl'][trim($matches[1])] = trim($matches[2]);
						}
						else
						{
							if (trim($line) === '')
							{
								// Start a new paragraph
								$p++;
							}
							else
							{
								// Make sure the current paragraph is set
								if ( ! isset($current_doc['html'][$current_block]['p'][$p]))
								{
									$current_doc['html'][$current_block]['p'][$p] = '';
								}

								// Add to the current paragraph
								$current_doc['html'][$current_block]['p'][$p] .= str_replace("\n", ' ', $line);
							}
						}
					}
				}
				else
				{
					switch (substr(trim($line), 0, 2))
					{
						case '//':
						case '* ': break;
						default:
							$line = trim($line);

							if ($this->is_function($line) OR $this->is_property($line) OR $this->is_class($line))
							{
								$clear = NULL;
								$this->current_doc =& $clear;

								// Restarts searching
								unset($closing_tag, $current_doc);
							}
						break;
					}
				}
			}

			$i++;
		}

		// Close the file
		fclose($file);
	}

	/**
	 * Method:
	 *  Checks if a line is a class, and parses the data out.
	 *
	 * Parameters:
	 *  line - a line from a file
	 *
	 * Returns:
	 *  TRUE or FALSE.
	 */
	protected function is_class($line)
	{
		if (strpos($line, 'class') === FALSE)
		{
			return FALSE;
		}

		$line = explode(' ', trim($line));

		$class = array
		(
			'name'    => '',
			'final'   => FALSE,
			'extends' => FALSE,
			'drivers' => FALSE
		);

		if (current($line) === 'final')
		{
			$class['final'] = (bool) array_shift($line);
		}

		if (current($line) === 'class')
		{
			// Remove "class"
			array_shift($line);

			$name = array_shift($line);
		}

		if (count($line) > 1)
		{
			// Remove "extends"
			array_shift($line);

			$class['extends'] = array_shift($line);
		}

		if (isset($name))
		{
			// Add the class into the docs
			$this->classes[$name] = array_merge($this->current_doc, $class);

			// Set the current class
			$this->current_class =& $this->classes[$name];

			return TRUE;
		}

		return FALSE;
	}

	/**
	 * Method:
	 *  Checks if a line is a property, and parses the data out.
	 *
	 * Parameters:
	 *  line - a line from a file
	 *
	 * Returns:
	 *  TRUE or FALSE.
	 */
	protected function is_property($line)
	{
		static $preg_vis;

		if ($preg_vis === NULL)
		{
			$preg_vis = 'var|'.implode('|', self::$php_visibility);
		}

		if (strpos($line, '$') === FALSE OR ! preg_match('/^(?:'.$preg_vis.')/', $line))
			return FALSE;

		$line = explode(' ', $line);

		$var = array
		(
			'visibility' => FALSE,
			'static'     => FALSE,
			'default'    => NULL
		);

		if (current($line) === 'var')
		{
			// Remove "var"
			array_shift($line);

			$var['visibility'] = 'public';
		}

		if (current($line) === 'static')
		{
			$var['visibility'] = (bool) array_shift($line);
		}

		// If the visibility is not set, this is not a
		if ($var['visibility'] === FALSE)
			return FALSE;

		if (substr(current($line), 0, 1) === '$')
		{
			$name = substr(array_shift($line), 1);
			$name = rtrim($name, ';');
		}

		if (count($line) AND current($line) === '=')
		{
			array_shift($line);

			$var['default'] = implode(' ', $line);
		}

		if (isset($name))
		{
			// Add property to class
			$this->current_class['properties'][$name] = array_merge($this->current_doc, $var);

			return TRUE;
		}

		return FALSE;
	}

	/**
	 * Method:
	 *  Checks if a line is a function, and parses the data out.
	 *
	 * Parameters:
	 *  line - a line from a file
	 *
	 * Returns:
	 *  TRUE or FALSE.
	 */
	protected function is_function($line)
	{
		if (strpos($line, 'function') === FALSE)
		{
			return FALSE;
		}

		$line = explode(' ', trim(strtolower($line)));

		$func = array
		(
			'final'      => FALSE,
			'visibility' => 'public',
			'static'     => FALSE,
		);

		if (current($line) === 'final')
		{
			$func['final'] = TRUE;
		}

		if (isset(self::$php_visibility[current($line)]))
		{
			$func['visibility'] = array_shift($line);
		}

		if (current($line) === 'static')
		{
			$func['static'] = (bool) array_shift($line);
		}

		if (current($line) === 'function')
		{
			// Remove "function"
			array_shift($line);

			// Get name
			$name = array_shift($line);

			// Remove arguments
			if (strpos($name, '(') !== FALSE)
			{
				$name = current(explode('(', $name, 2));
			}

			// Register the method
			$this->current_class['methods'][$name] = array_merge($this->current_doc, $func);

			return TRUE;
		}

		return FALSE;
	}

} // End Kodoc