<?php declare(strict_types=1);

namespace Rector\Rector\Argument;

use PhpParser\ConstExprEvaluator;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Stmt\ClassMethod;
use Rector\Rector\AbstractRector;
use Rector\RectorDefinition\ConfiguredCodeSample;
use Rector\RectorDefinition\RectorDefinition;

final class ArgumentRemoverRector extends AbstractRector
{
    /**
     * @var mixed[]
     */
    private $positionsByMethodNameByClassType = [];

    /**
     * @var ConstExprEvaluator
     */
    private $constExprEvaluator;

    /**
     * @param mixed[] $positionsByMethodNameByClassType
     */
    public function __construct(array $positionsByMethodNameByClassType, ConstExprEvaluator $constExprEvaluator)
    {
        $this->positionsByMethodNameByClassType = $positionsByMethodNameByClassType;
        $this->constExprEvaluator = $constExprEvaluator;
    }

    public function getDefinition(): RectorDefinition
    {
        return new RectorDefinition(
            'Removes defined arguments in defined methods and their calls.',
            [
                new ConfiguredCodeSample(
                    <<<'CODE_SAMPLE'
$someObject = new SomeClass;
$someObject->someMethod(true);
CODE_SAMPLE
                    ,
                    <<<'CODE_SAMPLE'
$someObject = new SomeClass;
$someObject->someMethod();'
CODE_SAMPLE
                    ,
                    [
                        'ExampleClass' => [
                            'someMethod' => [
                                0 => [
                                    'value' => 'true',
                                ],
                            ],
                        ],
                    ]
                ),
            ]
        );
    }

    /**
     * @return string[]
     */
    public function getNodeTypes(): array
    {
        return [MethodCall::class, StaticCall::class, ClassMethod::class];
    }

    /**
     * @param MethodCall|StaticCall|ClassMethod $node
     */
    public function refactor(Node $node): ?Node
    {
        foreach ($this->positionsByMethodNameByClassType as $type => $positionByMethodName) {
            if (! $this->isType($node, $type)) {
                continue;
            }

            foreach ($positionByMethodName as $methodName => $positions) {
                if (! $this->isName($node, $methodName)) {
                    continue;
                }

                foreach ($positions as $position => $match) {
                    $this->processPosition($node, $position, $match);
                }
            }
        }

        return $node;
    }

    /**
     * @param ClassMethod|StaticCall|MethodCall $node
     * @param mixed[]|null $match
     */
    private function processPosition(Node $node, int $position, ?array $match): void
    {
        if ($match === null) {
            if ($node instanceof MethodCall || $node instanceof StaticCall) {
                unset($node->args[$position]);
            } elseif ($node instanceof ClassMethod) {
                unset($node->params[$position]);
            }
        }

        if ($match) {
            // only argument specific value can be removed
            if ($node instanceof ClassMethod || ! isset($node->args[$position])) {
                return;
            }

            if ($this->isArgumentValueMatch($node->args[$position], $match)) {
                unset($node->args[$position]);
            }
        }
    }

    /**
     * @param mixed[] $values
     */
    private function isArgumentValueMatch(Arg $argNode, array $values): bool
    {
        $nodeValue = $this->constExprEvaluator->evaluateDirectly($argNode->value);

        return in_array($nodeValue, $values, true);
    }
}
