<?php

declare (strict_types=1);
/*
 * This file is part of PHP CS Fixer.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */
namespace PhpCsFixer\DocBlock;

use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
/**
 * This represents an entire annotation from a docblock.
 *
 * @author Graham Campbell <hello@gjcampbell.co.uk>
 * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
 *
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
 */
final class Annotation
{
    /**
     * All the annotation tag names with types.
     *
     * @var non-empty-list<string>
     */
    public const TAGS_WITH_TYPES = ['extends', 'implements', 'method', 'param', 'param-out', 'phpstan-type', 'phpstan-import-type', 'property', 'property-read', 'property-write', 'psalm-type', 'psalm-import-type', 'return', 'throws', 'type', 'var'];
    /**
     * The lines that make up the annotation.
     *
     * @var non-empty-list<Line>
     */
    private $lines;
    /**
     * The position of the first line of the annotation in the docblock.
     * @var int
     */
    private $start;
    /**
     * The position of the last line of the annotation in the docblock.
     * @var int
     */
    private $end;
    /**
     * The associated tag.
     * @var \PhpCsFixer\DocBlock\Tag|null
     */
    private $tag;
    /**
     * Lazy loaded, cached types content.
     * @var string|null
     */
    private $typesContent;
    /**
     * The cached types.
     *
     * @var null|list<string>
     */
    private $types;
    /**
     * @var \PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis|null
     */
    private $namespace;
    /**
     * @var list<NamespaceUseAnalysis>
     */
    private $namespaceUses;
    /**
     * Create a new line instance.
     *
     * @param non-empty-array<int, Line> $lines
     * @param null|NamespaceAnalysis     $namespace
     * @param list<NamespaceUseAnalysis> $namespaceUses
     */
    public function __construct(array $lines, $namespace = null, array $namespaceUses = [])
    {
        $this->lines = \array_values($lines);
        $this->namespace = $namespace;
        $this->namespaceUses = $namespaceUses;
        \reset($lines);
        $this->start = \key($lines);
        \end($lines);
        $this->end = \key($lines);
        \reset($lines);
    }
    /**
     * Get the string representation of object.
     */
    public function __toString() : string
    {
        return $this->getContent();
    }
    /**
     * Get all the annotation tag names with types.
     *
     * @return non-empty-list<string>
     *
     * @deprecated Use `Annotation::TAGS_WITH_TYPES` constant instead
     *
     * @TODO 4.0 remove me
     */
    public static function getTagsWithTypes() : array
    {
        return self::TAGS_WITH_TYPES;
    }
    /**
     * Get the start position of this annotation.
     */
    public function getStart() : int
    {
        return $this->start;
    }
    /**
     * Get the end position of this annotation.
     */
    public function getEnd() : int
    {
        return $this->end;
    }
    /**
     * Get the associated tag.
     */
    public function getTag() : \PhpCsFixer\DocBlock\Tag
    {
        if (null === $this->tag) {
            $this->tag = new \PhpCsFixer\DocBlock\Tag($this->lines[0]);
        }
        return $this->tag;
    }
    /**
     * @internal
     */
    public function getTypeExpression() : ?\PhpCsFixer\DocBlock\TypeExpression
    {
        $typesContent = $this->getTypesContent();
        return null === $typesContent ? null : new \PhpCsFixer\DocBlock\TypeExpression($typesContent, $this->namespace, $this->namespaceUses);
    }
    /**
     * @internal
     */
    public function getVariableName() : ?string
    {
        $type = \preg_quote($this->getTypesContent() ?? '', '/');
        $regex = \sprintf('/@%s\\s+(%s\\s*)?(&\\s*)?(\\.{3}\\s*)?(?<variable>\\$%s)(?:.*|$)/', $this->tag->getName(), $type, \PhpCsFixer\DocBlock\TypeExpression::REGEX_IDENTIFIER);
        if (Preg::match($regex, $this->getContent(), $matches)) {
            \assert(isset($matches['variable']));
            return $matches['variable'];
        }
        return null;
    }
    /**
     * Get the types associated with this annotation.
     *
     * @return list<string>
     */
    public function getTypes() : array
    {
        if (null === $this->types) {
            $typeExpression = $this->getTypeExpression();
            $this->types = null === $typeExpression ? [] : $typeExpression->getTypes();
        }
        return $this->types;
    }
    /**
     * Set the types associated with this annotation.
     *
     * @param list<string> $types
     */
    public function setTypes(array $types) : void
    {
        $origTypesContent = $this->getTypesContent();
        $newTypesContent = \implode(
            // Fallback to union type is provided for backward compatibility (previously glue was set to `|` by default even when type was not composite)
            // @TODO Better handling for cases where type is fixed (original type is not composite, but was made composite during fix)
            $this->getTypeExpression()->getTypesGlue() ?? '|',
            $types
        );
        if ($origTypesContent === $newTypesContent) {
            return;
        }
        $originalTypesLines = Preg::split('/([^\\n\\r]+\\R*)/', $origTypesContent, -1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE);
        $newTypesLines = Preg::split('/([^\\n\\r]+\\R*)/', $newTypesContent, -1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE);
        \assert(\count($originalTypesLines) === \count($newTypesLines));
        foreach ($newTypesLines as $index => $line) {
            \assert(isset($originalTypesLines[$index]));
            $pattern = '/' . \preg_quote($originalTypesLines[$index], '/') . '/';
            \assert(isset($this->lines[$index]));
            $this->lines[$index]->setContent(Preg::replace($pattern, $line, $this->lines[$index]->getContent(), 1));
        }
        $this->clearCache();
    }
    /**
     * Get the normalized types associated with this annotation, so they can easily be compared.
     *
     * @return list<string>
     */
    public function getNormalizedTypes() : array
    {
        $typeExpression = $this->getTypeExpression();
        if (null === $typeExpression) {
            return [];
        }
        $normalizedTypeExpression = $typeExpression->mapTypes(static function (\PhpCsFixer\DocBlock\TypeExpression $v) {
            return new \PhpCsFixer\DocBlock\TypeExpression(\strtolower($v->toString()), null, []);
        })->sortTypes(static function (\PhpCsFixer\DocBlock\TypeExpression $a, \PhpCsFixer\DocBlock\TypeExpression $b) {
            return $a->toString() <=> $b->toString();
        });
        return $normalizedTypeExpression->getTypes();
    }
    /**
     * Remove this annotation by removing all its lines.
     */
    public function remove() : void
    {
        foreach ($this->lines as $line) {
            if ($line->isTheStart() && $line->isTheEnd()) {
                // Single line doc block, remove entirely
                $line->remove();
            } elseif ($line->isTheStart()) {
                // Multi line doc block, but start is on the same line as the first annotation, keep only the start
                $content = Preg::replace('#(\\s*/\\*\\*).*#', '$1', $line->getContent());
                $line->setContent($content);
            } elseif ($line->isTheEnd()) {
                // Multi line doc block, but end is on the same line as the last annotation, keep only the end
                $content = Preg::replace('#(\\s*)\\S.*(\\*/.*)#', '$1$2', $line->getContent());
                $line->setContent($content);
            } else {
                // Multi line doc block, neither start nor end on this line, can be removed safely
                $line->remove();
            }
        }
        $this->clearCache();
    }
    /**
     * Get the annotation content.
     */
    public function getContent() : string
    {
        return \implode('', $this->lines);
    }
    public function supportTypes() : bool
    {
        return \in_array($this->getTag()->getName(), self::TAGS_WITH_TYPES, \true);
    }
    /**
     * Get the current types content.
     *
     * Be careful modifying the underlying line as that won't flush the cache.
     */
    private function getTypesContent() : ?string
    {
        if (null === $this->typesContent) {
            $name = $this->getTag()->getName();
            if (!$this->supportTypes()) {
                throw new \RuntimeException('This tag does not support types.');
            }
            if (Preg::match('{^(?:\\h*\\*|/\\*\\*)[\\h*]*@' . $name . '\\h+' . \PhpCsFixer\DocBlock\TypeExpression::REGEX_TYPES . '(?:(?:[*\\h\\v]|\\&?[\\.\\$\\s]).*)?\\r?$}is', $this->getContent(), $matches)) {
                \assert(isset($matches['types']));
                $this->typesContent = $matches['types'];
            }
        }
        return $this->typesContent;
    }
    private function clearCache() : void
    {
        $this->types = null;
        $this->typesContent = null;
    }
}
