<?php

namespace Laminas\Code\Reflection;

use Laminas\Code\Reflection\DocBlock\Tag\TagInterface as DocBlockTagInterface;
use Laminas\Code\Reflection\DocBlock\TagManager as DocBlockTagManager;
use Laminas\Code\Scanner\DocBlockScanner;
use Reflector;

use function count;
use function is_string;
use function ltrim;
use function method_exists;
use function preg_replace;
use function sprintf;
use function substr_count;

class DocBlockReflection implements ReflectionInterface
{
    /** @var Reflector */
    protected $reflector;

    /** @var string */
    protected $docComment;

    /** @var DocBlockTagManager */
    protected $tagManager;

    /** @var int */
    protected $startLine;

    /** @var int */
    protected $endLine;

    /** @var string */
    protected $cleanDocComment;

    /** @var string */
    protected $longDescription;

    /** @var string */
    protected $shortDescription;

    /** @var array */
    protected $tags = [];

    /** @var bool */
    protected $isReflected = false;

    /**
     * Export reflection
     *
     * Required by the Reflector interface.
     *
     * @todo   What should this do?
     * @return void
     */
    public static function export()
    {
    }

    /**
     * @param  Reflector|string $commentOrReflector
     * @throws Exception\InvalidArgumentException
     */
    public function __construct($commentOrReflector, ?DocBlockTagManager $tagManager = null)
    {
        if (! $tagManager) {
            $tagManager = new DocBlockTagManager();
            $tagManager->initializeDefaultTags();
        }
        $this->tagManager = $tagManager;

        if ($commentOrReflector instanceof Reflector) {
            $this->reflector = $commentOrReflector;
            if (! method_exists($commentOrReflector, 'getDocComment')) {
                throw new Exception\InvalidArgumentException('Reflector must contain method "getDocComment"');
            }

            $this->docComment = $commentOrReflector->getDocComment();

            // determine line numbers
            $lineCount       = substr_count($this->docComment, "\n");
            $this->startLine = $this->reflector->getStartLine() - $lineCount - 1;
            $this->endLine   = $this->reflector->getStartLine() - 1;
        } elseif (is_string($commentOrReflector)) {
            $this->docComment = $commentOrReflector;
        } else {
            throw new Exception\InvalidArgumentException(sprintf(
                '%s must have a (string) DocComment or a Reflector in the constructor',
                static::class
            ));
        }

        if ($this->docComment == '') {
            throw new Exception\InvalidArgumentException('DocComment cannot be empty');
        }

        $this->reflect();
    }

    /**
     * Retrieve contents of DocBlock
     *
     * @return string
     */
    public function getContents()
    {
        $this->reflect();

        return $this->cleanDocComment;
    }

    /**
     * Get start line (position) of DocBlock
     *
     * @return int
     */
    public function getStartLine()
    {
        $this->reflect();

        return $this->startLine;
    }

    /**
     * Get last line (position) of DocBlock
     *
     * @return int
     */
    public function getEndLine()
    {
        $this->reflect();

        return $this->endLine;
    }

    /**
     * Get DocBlock short description
     *
     * @return string
     */
    public function getShortDescription()
    {
        $this->reflect();

        return $this->shortDescription;
    }

    /**
     * Get DocBlock long description
     *
     * @return string
     */
    public function getLongDescription()
    {
        $this->reflect();

        return $this->longDescription;
    }

    /**
     * Does the DocBlock contain the given annotation tag?
     *
     * @param  string $name
     * @return bool
     */
    public function hasTag($name)
    {
        $this->reflect();
        foreach ($this->tags as $tag) {
            if ($tag->getName() == $name) {
                return true;
            }
        }

        return false;
    }

    /**
     * Retrieve the given DocBlock tag
     *
     * @param  string $name
     * @return DocBlockTagInterface|false
     */
    public function getTag($name)
    {
        $this->reflect();
        foreach ($this->tags as $tag) {
            if ($tag->getName() == $name) {
                return $tag;
            }
        }

        return false;
    }

    /**
     * Get all DocBlock annotation tags
     *
     * @param  string $filter
     * @return DocBlockTagInterface[]
     */
    public function getTags($filter = null)
    {
        $this->reflect();
        if ($filter === null || ! is_string($filter)) {
            return $this->tags;
        }

        $returnTags = [];
        foreach ($this->tags as $tag) {
            if ($tag->getName() == $filter) {
                $returnTags[] = $tag;
            }
        }

        return $returnTags;
    }

    /**
     * Parse the DocBlock
     *
     * @return void
     */
    protected function reflect()
    {
        if ($this->isReflected) {
            return;
        }

        $docComment = preg_replace('#[ ]{0,1}\*/$#', '', $this->docComment);

        // create a clean docComment
        $this->cleanDocComment = preg_replace("#[ \t]*(?:/\*\*|\*/|\*)[ ]{0,1}(.*)?#", '$1', $docComment);

        // @todo should be changed to remove first and last empty line
        $this->cleanDocComment = ltrim($this->cleanDocComment, "\r\n");

        $scanner                = new DocBlockScanner($docComment);
        $this->shortDescription = ltrim($scanner->getShortDescription());
        $this->longDescription  = ltrim($scanner->getLongDescription());

        foreach ($scanner->getTags() as $tag) {
            $this->tags[] = $this->tagManager->createTag(ltrim($tag['name'], '@'), ltrim($tag['value']));
        }

        $this->isReflected = true;
    }

    /**
     * @return string
     */
    public function toString()
    {
        $str  = 'DocBlock [ /* DocBlock */ ] {' . "\n\n";
        $str .= '  - Tags [' . count($this->tags) . '] {' . "\n";

        foreach ($this->tags as $tag) {
            $str .= '    ' . $tag;
        }

        $str .= '  }' . "\n";
        $str .= '}' . "\n";

        return $str;
    }

    /**
     * Serialize to string
     *
     * Required by the Reflector interface
     */
    public function __toString(): string
    {
        return $this->toString();
    }
}
