<?php declare(strict_types=1);

/*
 * This file is part of Composer.
 *
 * (c) Nils Adermann <naderman@naderman.de>
 *     Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Composer\PHPStan;

use Composer\DependencyResolver\Rule;
use Composer\Package\BasePackage;
use Composer\Package\Link;
use Composer\Semver\Constraint\ConstraintInterface;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\IntegerType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\ObjectType;
use PHPStan\Type\TypeCombinator;
use PhpParser\Node\Identifier;

final class RuleReasonDataReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
    public function getClass(): string
    {
        return Rule::class;
    }

    public function isMethodSupported(MethodReflection $methodReflection): bool
    {
        return strtolower($methodReflection->getName()) === 'getreasondata';
    }

    public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
    {
        $reasonType = $scope->getType(new MethodCall($methodCall->var, new Identifier('getReason')));

        $types = [
            Rule::RULE_ROOT_REQUIRE => new ConstantArrayType([new ConstantStringType('packageName'), new ConstantStringType('constraint')], [new StringType, new ObjectType(ConstraintInterface::class)]),
            Rule::RULE_FIXED => new ConstantArrayType([new ConstantStringType('package')], [new ObjectType(BasePackage::class)]),
            Rule::RULE_LOCKED_FILTER_LIST_REMOVED => new ConstantArrayType([new ConstantStringType('package')], [new ObjectType(BasePackage::class)]),
            Rule::RULE_PACKAGE_CONFLICT => new ObjectType(Link::class),
            Rule::RULE_PACKAGE_REQUIRES => new ObjectType(Link::class),
            Rule::RULE_PACKAGE_SAME_NAME => TypeCombinator::intersect(new StringType, new AccessoryNonEmptyStringType()),
            Rule::RULE_LEARNED => new IntegerType(),
            Rule::RULE_PACKAGE_ALIAS => new ObjectType(BasePackage::class),
            Rule::RULE_PACKAGE_INVERSE_ALIAS => new ObjectType(BasePackage::class),
        ];

        // Collect every reasonData type whose reason constant is a possible
        // value of the call-site's getReason(). Single-case `case RULE_FIXED:`
        // narrows reasonType to one constant and yields a single match;
        // fall-through `case RULE_FIXED: case RULE_LOCKED_FILTER_LIST_REMOVED:`
        // yields a union of constants and we return the union of their
        // reasonData types so offset access can still narrow correctly.
        $matched = [];
        foreach ($types as $const => $type) {
            if ($reasonType->isSuperTypeOf(new ConstantIntegerType($const))->yes()) {
                $matched[] = $type;
            }
        }

        if (\count($matched) > 0) {
            return \count($matched) === 1 ? $matched[0] : TypeCombinator::union(...$matched);
        }

        return TypeCombinator::union(...$types);
    }
}
