<?php
/**
 * Class documentation generator.
 *
 * @package    Kohana/Userguide
 * @category   Base
 * @author     Kohana Team
 * @copyright  (c) Kohana Team
 * @license    https://koseven.ga/LICENSE.md
 */
class Kohana_Kodoc_Class extends Kodoc {

	/**
	 * @var  ReflectionClass The ReflectionClass for this class
	 */
	public $class;

	/**
	 * @var  string  modifiers like abstract, final
	 */
	public $modifiers;

	/**
	 * @var  string  description of the class from the comment
	 */
	public $description;

	/**
	 * @var  array  array of tags, retrieved from the comment
	 */
	public $tags = [];

	/**
	 * @var  array  array of this classes constants
	 */
	public $constants = [];

	/**
	 * @var array Parent classes/interfaces of this class/interface
	 */
	public $parents = [];

	/**
	 * Loads a class and uses [reflection](http://php.net/reflection) to parse
	 * the class. Reads the class modifiers, constants and comment. Parses the
	 * comment to find the description and tags.
	 *
	 * @param   string  Class name
	 * @return  void
	 */
	public function __construct($class)
	{
		$this->class = new ReflectionClass($class);

		if ($modifiers = $this->class->getModifiers())
		{
			$this->modifiers = '<small>'.implode(' ', Reflection::getModifierNames($modifiers)).'</small> ';
		}

		$this->constants = $this->class->getConstants();

		// If ReflectionClass::getParentClass() won't work if the class in
		// question is an interface
		if ($this->class->isInterface())
		{
			$this->parents = $this->class->getInterfaces();
		}
		else
		{
			$parent = $this->class;

			while ($parent = $parent->getParentClass())
			{
				$this->parents[] = $parent;
			}
		}

		if ( ! $comment = $this->class->getDocComment())
		{
			foreach ($this->parents as $parent)
			{
				if ($comment = $parent->getDocComment())
				{
					// Found a description for this class
					break;
				}
			}
		}

		list($this->description, $this->tags) = Kodoc::parse($comment, FALSE);
	}

	/**
	 * Gets the constants of this class as HTML.
	 *
	 * @return  array
	 */
	public function constants()
	{
		$result = [];

		foreach ($this->constants as $name => $value)
		{
			$result[$name] = Debug::vars($value);
		}

		return $result;
	}

	/**
	 * Get the description of this class as HTML. Includes a warning when the
	 * class or one of its parents could not be found.
	 *
	 * @return  string  HTML
	 */
	public function description()
	{
		$result = $this->description;

		// If this class extends Kodoc_Missing, add a warning about possible
		// incomplete documentation
		foreach ($this->parents as $parent)
		{
			if ($parent->name == 'Kodoc_Missing')
			{
				$result .= "[!!] **This class, or a class parent, could not be
				           found or loaded. This could be caused by a missing
				           module or other dependancy. The documentation for
				           class may not be complete!**";
			}
		}

		return Kodoc_Markdown::markdown($result);
	}

	/**
	 * Gets a list of the class properties as [Kodoc_Property] objects.
	 *
	 * @return  array
	 */
	public function properties()
	{
		$props = $this->class->getProperties();

		$defaults = $this->class->getDefaultProperties();

		usort($props, [$this,'_prop_sort']);

		foreach ($props as $key => $property)
		{
			// Create Kodoc Properties for each property
			$props[$key] = new Kodoc_Property($this->class->name, $property->name,  Arr::get($defaults, $property->name));
		}

		return $props;
	}

	protected function _prop_sort($a, $b)
	{
		// If one property is public, and the other is not, it goes on top
		if ($a->isPublic() AND ( ! $b->isPublic()))
			return -1;
		if ($b->isPublic() AND ( ! $a->isPublic()))
			return 1;

		// If one property is protected and the other is private, it goes on top
		if ($a->isProtected() AND $b->isPrivate())
			return -1;
		if ($b->isProtected() AND $a->isPrivate())
			return 1;

		// Otherwise just do alphabetical
		return strcmp($a->name, $b->name);
	}

	/**
	 * Gets a list of the class properties as [Kodoc_Method] objects.
	 *
	 * @return  array
	 */
	public function methods()
	{
		$methods = $this->class->getMethods();

		usort($methods, [$this,'_method_sort']);

		foreach ($methods as $key => $method)
		{
			$methods[$key] = new Kodoc_Method($this->class->name, $method->name);
		}

		return $methods;
	}

	/**
	 * Sort methods based on their visibility and declaring class based on:
	 *
	 *  * methods will be sorted public, protected, then private.
	 *  * methods that are declared by an ancestor will be after classes
	 *    declared by the current class
	 *  * lastly, they will be sorted alphabetically
	 *
	 */
	protected function _method_sort($a, $b)
	{
		// If one method is public, and the other is not, it goes on top
		if ($a->isPublic() AND ( ! $b->isPublic()))
			return -1;
		if ($b->isPublic() AND ( ! $a->isPublic()))
			return 1;

		// If one method is protected and the other is private, it goes on top
		if ($a->isProtected() AND $b->isPrivate())
			return -1;
		if ($b->isProtected() AND $a->isPrivate())
			return 1;

		// The methods have the same visibility, so check the declaring class depth:


		/*
		echo Debug::vars('a is '.$a->class.'::'.$a->name,'b is '.$b->class.'::'.$b->name,
						   'are the classes the same?', $a->class == $b->class,'if they are, the result is:',strcmp($a->name, $b->name),
						   'is a this class?', $a->name == $this->class->name,-1,
						   'is b this class?', $b->name == $this->class->name,1,
						   'otherwise, the result is:',strcmp($a->class, $b->class)
						   );
		*/

		// If both methods are defined in the same class, just compare the method names
		if ($a->class == $b->class)
			return strcmp($a->name, $b->name);

		// If one of them was declared by this class, it needs to be on top
		if ($a->name == $this->class->name)
			return -1;
		if ($b->name == $this->class->name)
			return 1;

		// Otherwise, get the parents of each methods declaring class, then compare which function has more "ancestors"
		$adepth = 0;
		$bdepth = 0;

		$parent = $a->getDeclaringClass();
		do
		{
			$adepth++;
		}
		while ($parent = $parent->getParentClass());

		$parent = $b->getDeclaringClass();
		do
		{
			$bdepth++;
		}
		while ($parent = $parent->getParentClass());

		return $bdepth - $adepth;
	}

	/**
	 * Get the tags of this class as HTML.
	 *
	 * @return  array
	 */
	public function tags()
	{
		$result = [];

		foreach ($this->tags as $name => $set)
		{
			foreach ($set as $text)
			{
				$result[$name][] = Kodoc::format_tag($name, $text);
			}
		}

		return $result;
	}

} // End Kodoc_Class
