<?php

declare (strict_types=1);
namespace Rector\BetterPhpDocParser\PhpDocManipulator;

use PhpParser\Node\FunctionLike;
use PhpParser\Node\Param;
use PhpParser\Node\Stmt;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\MixedType;
use PHPStan\Type\NeverType;
use PHPStan\Type\Type;
use Rector\BetterPhpDocParser\Guard\NewPhpDocFromPHPStanTypeGuard;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
use Rector\BetterPhpDocParser\ValueObject\Type\BracketsAwareIntersectionTypeNode;
use Rector\BetterPhpDocParser\ValueObject\Type\BracketsAwareUnionTypeNode;
use Rector\BetterPhpDocParser\ValueObject\Type\SpacingAwareArrayTypeNode;
use Rector\BetterPhpDocParser\ValueObject\Type\SpacingAwareCallableTypeNode;
use Rector\Comments\NodeDocBlock\DocBlockUpdater;
use Rector\NodeTypeResolver\TypeComparator\TypeComparator;
use Rector\StaticTypeMapper\StaticTypeMapper;
use Rector\TypeDeclaration\PhpDocParser\ParamPhpDocNodeFactory;
final class PhpDocTypeChanger
{
    /**
     * @readonly
     */
    private StaticTypeMapper $staticTypeMapper;
    /**
     * @readonly
     */
    private TypeComparator $typeComparator;
    /**
     * @readonly
     */
    private ParamPhpDocNodeFactory $paramPhpDocNodeFactory;
    /**
     * @readonly
     */
    private NewPhpDocFromPHPStanTypeGuard $newPhpDocFromPHPStanTypeGuard;
    /**
     * @readonly
     */
    private DocBlockUpdater $docBlockUpdater;
    /**
     * @var array<class-string<Node>>
     */
    private const ALLOWED_TYPES = [GenericTypeNode::class, SpacingAwareArrayTypeNode::class, SpacingAwareCallableTypeNode::class, ArrayShapeNode::class];
    /**
     * @var string[]
     */
    private const ALLOWED_IDENTIFIER_TYPENODE_TYPES = ['class-string'];
    public function __construct(StaticTypeMapper $staticTypeMapper, TypeComparator $typeComparator, ParamPhpDocNodeFactory $paramPhpDocNodeFactory, NewPhpDocFromPHPStanTypeGuard $newPhpDocFromPHPStanTypeGuard, DocBlockUpdater $docBlockUpdater)
    {
        $this->staticTypeMapper = $staticTypeMapper;
        $this->typeComparator = $typeComparator;
        $this->paramPhpDocNodeFactory = $paramPhpDocNodeFactory;
        $this->newPhpDocFromPHPStanTypeGuard = $newPhpDocFromPHPStanTypeGuard;
        $this->docBlockUpdater = $docBlockUpdater;
    }
    public function changeVarType(Stmt $stmt, PhpDocInfo $phpDocInfo, Type $newType): bool
    {
        // better skip, could crash hard
        if ($phpDocInfo->hasInvalidTag('@var')) {
            return \false;
        }
        // make sure the tags are not identical, e.g imported class vs FQN class
        if ($this->typeComparator->areTypesEqual($phpDocInfo->getVarType(), $newType)) {
            return \false;
        }
        // prevent existing type override by mixed
        if (!$phpDocInfo->getVarType() instanceof MixedType && $newType instanceof ConstantArrayType && $newType->getIterableValueType() instanceof NeverType) {
            return \false;
        }
        if (!$this->newPhpDocFromPHPStanTypeGuard->isLegal($newType)) {
            return \false;
        }
        // override existing type
        $newPHPStanPhpDocTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPHPStanPhpDocTypeNode($newType);
        $currentVarTagValueNode = $phpDocInfo->getVarTagValueNode();
        if ($currentVarTagValueNode instanceof VarTagValueNode) {
            // only change type
            $currentVarTagValueNode->type = $newPHPStanPhpDocTypeNode;
        } else {
            // add completely new one
            $varTagValueNode = new VarTagValueNode($newPHPStanPhpDocTypeNode, '', '');
            $phpDocInfo->addTagValueNode($varTagValueNode);
        }
        $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($stmt);
        return \true;
    }
    public function changeReturnTypeNode(FunctionLike $functionLike, PhpDocInfo $phpDocInfo, TypeNode $newTypeNode): void
    {
        $existingReturnTagValueNode = $phpDocInfo->getReturnTagValue();
        if ($existingReturnTagValueNode instanceof ReturnTagValueNode) {
            // enforce reprint of copied type node
            $newTypeNode->setAttribute('orig_node', null);
            $existingReturnTagValueNode->type = $newTypeNode;
        } else {
            $returnTagValueNode = new ReturnTagValueNode($newTypeNode, '');
            $phpDocInfo->addTagValueNode($returnTagValueNode);
        }
        $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($functionLike);
    }
    public function changeParamTypeNode(FunctionLike $functionLike, PhpDocInfo $phpDocInfo, Param $param, string $paramName, TypeNode $newTypeNode): void
    {
        $existingParamTagValueNode = $phpDocInfo->getParamTagValueByName($paramName);
        if ($existingParamTagValueNode instanceof ParamTagValueNode) {
            $existingParamTagValueNode->type = $newTypeNode;
        } else {
            $paramTagValueNode = $this->paramPhpDocNodeFactory->create($newTypeNode, $param);
            $phpDocInfo->addTagValueNode($paramTagValueNode);
        }
        $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($functionLike);
    }
    public function changeReturnType(FunctionLike $functionLike, PhpDocInfo $phpDocInfo, Type $newType): bool
    {
        // better not touch this, can crash
        if ($phpDocInfo->hasInvalidTag('@return')) {
            return \false;
        }
        // make sure the tags are not identical, e.g imported class vs FQN class
        if ($this->typeComparator->areTypesEqual($phpDocInfo->getReturnType(), $newType)) {
            return \false;
        }
        if (!$this->newPhpDocFromPHPStanTypeGuard->isLegal($newType)) {
            return \false;
        }
        // override existing type
        $newPHPStanPhpDocTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPHPStanPhpDocTypeNode($newType);
        $currentReturnTagValueNode = $phpDocInfo->getReturnTagValue();
        if ($currentReturnTagValueNode instanceof ReturnTagValueNode) {
            // only change type
            $currentReturnTagValueNode->type = $newPHPStanPhpDocTypeNode;
        } else {
            // add completely new one
            $returnTagValueNode = new ReturnTagValueNode($newPHPStanPhpDocTypeNode, '');
            $phpDocInfo->addTagValueNode($returnTagValueNode);
        }
        $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($functionLike);
        return \true;
    }
    public function changeParamType(FunctionLike $functionLike, PhpDocInfo $phpDocInfo, Type $newType, Param $param, string $paramName): bool
    {
        // better skip, could crash hard
        if ($phpDocInfo->hasInvalidTag('@param')) {
            return \false;
        }
        if (!$this->newPhpDocFromPHPStanTypeGuard->isLegal($newType)) {
            return \false;
        }
        $phpDocTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPHPStanPhpDocTypeNode($newType);
        $paramTagValueNode = $phpDocInfo->getParamTagValueByName($paramName);
        // override existing type
        if ($paramTagValueNode instanceof ParamTagValueNode) {
            // already set
            $currentType = $this->staticTypeMapper->mapPHPStanPhpDocTypeNodeToPHPStanType($paramTagValueNode->type, $param);
            // avoid overriding better type
            if ($this->typeComparator->isSubtype($currentType, $newType)) {
                return \false;
            }
            if ($this->typeComparator->areTypesEqual($currentType, $newType)) {
                return \false;
            }
            $paramTagValueNode->type = $phpDocTypeNode;
        } else {
            $paramTagValueNode = $this->paramPhpDocNodeFactory->create($phpDocTypeNode, $param);
            $phpDocInfo->addTagValueNode($paramTagValueNode);
        }
        $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($functionLike);
        return \true;
    }
    public function isAllowed(TypeNode $typeNode): bool
    {
        if ($typeNode instanceof BracketsAwareUnionTypeNode || $typeNode instanceof BracketsAwareIntersectionTypeNode) {
            foreach ($typeNode->types as $type) {
                if ($this->isAllowed($type)) {
                    return \true;
                }
            }
        }
        if ($typeNode instanceof ConstTypeNode && $typeNode->constExpr instanceof ConstFetchNode) {
            return \true;
        }
        if (in_array(get_class($typeNode), self::ALLOWED_TYPES, \true)) {
            return \true;
        }
        if (!$typeNode instanceof IdentifierTypeNode) {
            return \false;
        }
        return in_array((string) $typeNode, self::ALLOWED_IDENTIFIER_TYPENODE_TYPES, \true);
    }
    /**
     * @api downgrade
     */
    public function changeVarTypeNode(Stmt $stmt, PhpDocInfo $phpDocInfo, TypeNode $typeNode): void
    {
        $existingVarTagValueNode = $phpDocInfo->getVarTagValueNode();
        if ($existingVarTagValueNode instanceof VarTagValueNode) {
            $existingVarTagValueNode->type = $typeNode;
        } else {
            // add completely new one
            $varTagValueNode = new VarTagValueNode($typeNode, '', '');
            $phpDocInfo->addTagValueNode($varTagValueNode);
        }
        $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($stmt);
    }
}
