<?php
/**
 * phpDocumentor
 *
 * PHP Version 5.3
 *
 * @author    Mike van Riel <mike.vanriel@naenius.com>
 * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com)
 * @license   http://www.opensource.org/licenses/mit-license.php MIT
 * @link      http://phpdoc.org
 */

namespace Barryvdh\Reflection\DocBlock;

use Barryvdh\Reflection\DocBlock;

/**
 * Parses a tag definition for a DocBlock.
 *
 * @author  Mike van Riel <mike.vanriel@naenius.com>
 * @license http://www.opensource.org/licenses/mit-license.php MIT
 * @link    http://phpdoc.org
 */
class Tag implements \Reflector
{
    /**
     * PCRE regular expression matching a tag name.
     */
    const REGEX_TAGNAME = '[\w\-\_\\\\]+';

    /** @var string Name of the tag */
    protected $tag = '';

    /**
     * @var string|null Content of the tag.
     *     When set to NULL, it means it needs to be regenerated.
     */
    protected $content = '';

    /** @var string Description of the content of this tag */
    protected $description = '';

    /**
     * @var array|null The description, as an array of strings and Tag objects.
     *     When set to NULL, it means it needs to be regenerated.
     */
    protected $parsedDescription = null;

    /** @var Location Location of the tag. */
    protected $location = null;

    /** @var DocBlock The DocBlock which this tag belongs to. */
    protected $docblock = null;
    
    /**
     * @var array An array with a tag as a key, and an FQCN to a class that
     *     handles it as an array value. The class is expected to inherit this
     *     class.
     */
    private static $tagHandlerMappings = array(
        'author'
            => '\Barryvdh\Reflection\DocBlock\Tag\AuthorTag',
        'covers'
            => '\Barryvdh\Reflection\DocBlock\Tag\CoversTag',
        'deprecated'
            => '\Barryvdh\Reflection\DocBlock\Tag\DeprecatedTag',
        'example'
            => '\Barryvdh\Reflection\DocBlock\Tag\ExampleTag',
        'link'
            => '\Barryvdh\Reflection\DocBlock\Tag\LinkTag',
        'method'
            => '\Barryvdh\Reflection\DocBlock\Tag\MethodTag',
        'param'
            => '\Barryvdh\Reflection\DocBlock\Tag\ParamTag',
        'property-read'
            => '\Barryvdh\Reflection\DocBlock\Tag\PropertyReadTag',
        'property'
            => '\Barryvdh\Reflection\DocBlock\Tag\PropertyTag',
        'property-write'
            => '\Barryvdh\Reflection\DocBlock\Tag\PropertyWriteTag',
        'return'
            => '\Barryvdh\Reflection\DocBlock\Tag\ReturnTag',
        'see'
            => '\Barryvdh\Reflection\DocBlock\Tag\SeeTag',
        'since'
            => '\Barryvdh\Reflection\DocBlock\Tag\SinceTag',
        'source'
            => '\Barryvdh\Reflection\DocBlock\Tag\SourceTag',
        'throw'
            => '\Barryvdh\Reflection\DocBlock\Tag\ThrowsTag',
        'throws'
            => '\Barryvdh\Reflection\DocBlock\Tag\ThrowsTag',
        'uses'
            => '\Barryvdh\Reflection\DocBlock\Tag\UsesTag',
        'var'
            => '\Barryvdh\Reflection\DocBlock\Tag\VarTag',
        'version'
            => '\Barryvdh\Reflection\DocBlock\Tag\VersionTag',
        'SuppressWarnings'
            => '\Barryvdh\Reflection\DocBlock\Tag\SuppressWarningsTag',
        'template'
            => '\Barryvdh\Reflection\DocBlock\Tag\TemplateTag'
    );

    /**
     * Factory method responsible for instantiating the correct sub type.
     *
     * @param string   $tag_line The text for this tag, including description.
     * @param DocBlock $docblock The DocBlock which this tag belongs to.
     * @param Location $location Location of the tag.
     *
     * @throws \InvalidArgumentException if an invalid tag line was presented.
     *
     * @return static A new tag object.
     */
    final public static function createInstance(
        $tag_line,
        ?DocBlock $docblock = null,
        ?Location $location = null
    ) {
        if (!preg_match(
            '/^@(' . self::REGEX_TAGNAME . ')(?:\s*([^\s].*)|$)?/us',
            $tag_line,
            $matches
        )) {
            throw new \InvalidArgumentException(
                'Invalid tag_line detected: ' . $tag_line
            );
        }

        $handler = __CLASS__;
        if (isset(self::$tagHandlerMappings[$matches[1]])) {
            $handler = self::$tagHandlerMappings[$matches[1]];
        } elseif (isset($docblock)) {
            $tagName = (string)new Type\Collection(
                array($matches[1]),
                $docblock->getContext()
            );

            if (isset(self::$tagHandlerMappings[$tagName])) {
                $handler = self::$tagHandlerMappings[$tagName];
            }
        }

        return new $handler(
            $matches[1],
            isset($matches[2]) ? $matches[2] : '',
            $docblock,
            $location
        );
    }

    /**
     * Registers a handler for tags.
     * 
     * Registers a handler for tags. The class specified is autoloaded if it's
     * not available. It must inherit from this class.
     * 
     * @param string      $tag     Name of tag to regiser a handler for. When
     *     registering a namespaced tag, the full name, along with a prefixing
     *     slash MUST be provided.
     * @param string|null $handler FQCN of handler. Specifing NULL removes the
     *     handler for the specified tag, if any.
     * 
     * @return bool TRUE on success, FALSE on failure.
     */
    final public static function registerTagHandler($tag, $handler)
    {
        $tag = trim((string)$tag);

        if (null === $handler) {
            unset(self::$tagHandlerMappings[$tag]);
            return true;
        }

        if ('' !== $tag
            && class_exists($handler, true)
            && is_subclass_of($handler, __CLASS__)
            && !strpos($tag, '\\') //Accept no slash, and 1st slash at offset 0.
        ) {
            self::$tagHandlerMappings[$tag] = $handler;
            return true;
        }

        return false;
    }

    /**
     * Parses a tag and populates the member variables.
     *
     * @param string   $name     Name of the tag.
     * @param string   $content  The contents of the given tag.
     * @param DocBlock $docblock The DocBlock which this tag belongs to.
     * @param Location $location Location of the tag.
     */
    public function __construct(
        $name,
        $content,
        ?DocBlock $docblock = null,
        ?Location $location = null
    ) {
        $this
            ->setName($name)
            ->setContent($content)
            ->setDocBlock($docblock)
            ->setLocation($location);
    }

    /**
     * Gets the name of this tag.
     *
     * @return string The name of this tag.
     */
    public function getName()
    {
        return $this->tag;
    }

    /**
     * Sets the name of this tag.
     * 
     * @param string $name The new name of this tag.
     * 
     * @return $this
     * @throws \InvalidArgumentException When an invalid tag name is provided.
     */
    public function setName($name)
    {
        if (!preg_match('/^' . self::REGEX_TAGNAME . '$/u', $name)) {
            throw new \InvalidArgumentException(
                'Invalid tag name supplied: ' . $name
            );
        }

        $this->tag = $name;

        return $this;
    }

    /**
     * Gets the content of this tag.
     *
     * @return string
     */
    public function getContent()
    {
        if (null === $this->content) {
            $this->content = $this->description;
        }

        return $this->content;
    }

    /**
     * Sets the content of this tag.
     * 
     * @param string $content The new content of this tag.
     * 
     * @return $this
     */
    public function setContent($content)
    {
        $this->setDescription($content);
        $this->content = $content;

        return $this;
    }

    /**
     * Gets the description component of this tag.
     *
     * @return string
     */
    public function getDescription()
    {
        return $this->description;
    }

    /**
     * Sets the description component of this tag.
     * 
     * @param string $description The new description component of this tag.
     * 
     * @return $this
     */
    public function setDescription($description)
    {
        $this->content = null;
        $this->parsedDescription = null;
        $this->description = trim($description);

        return $this;
    }

    /**
     * Gets the parsed text of this description.
     * 
     * @return array An array of strings and tag objects, in the order they
     *     occur within the description.
     */
    public function getParsedDescription()
    {
        if (null === $this->parsedDescription) {
            $description = new Description($this->description, $this->docblock);
            $this->parsedDescription = $description->getParsedContents();
        }
        return $this->parsedDescription;
    }

    /**
     * Gets the docblock this tag belongs to.
     * 
     * @return DocBlock The docblock this tag belongs to.
     */
    public function getDocBlock()
    {
        return $this->docblock;
    }

    /**
     * Sets the docblock this tag belongs to.
     * 
     * @param DocBlock $docblock The new docblock this tag belongs to. Setting
     *     NULL removes any association.
     * 
     * @return $this
     */
    public function setDocBlock(?DocBlock $docblock = null)
    {
        $this->docblock = $docblock;

        return $this;
    }

    /**
     * Gets the location of the tag.
     *
     * @return Location The tag's location.
     */
    public function getLocation()
    {
        return $this->location;
    }
    
    /**
     * Sets the location of the tag.
     * 
     * @param Location $location The new location of the tag.
     * 
     * @return $this
     */
    public function setLocation(?Location $location = null)
    {
        $this->location = $location;

        return $this;
    }

    /**
     * If the given tags should be together or apart.
     *
     * @param Tag $tag
     *
     * @return bool
     */
    public function inSameGroup(Tag $tag)
    {
        $firstName = $this->getName();
        $secondName = $tag->getName();

        if ($firstName === $secondName) {
            return true;
        }

        $groups = array(
            array('deprecated', 'link', 'see', 'since'),
            array('author', 'copyright', 'license'),
            array('category', 'package', 'subpackage'),
            array('property', 'property-read', 'property-write'),
            array('param', 'return'),
        );

        foreach ($groups as $group) {
            if (in_array($firstName, $group, true) && in_array($secondName, $group, true)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Builds a string representation of this object.
     *
     * @todo determine the exact format as used by PHP Reflection and implement it.
     *
     * @return void
     * @codeCoverageIgnore Not yet implemented
     */
    public static function export()
    {
        throw new \Exception('Not yet implemented');
    }

    /**
     * Returns the tag as a serialized string
     *
     * @return string
     */
    public function __toString()
    {
        return "@{$this->getName()} {$this->getContent()}";
    }
}
