<?php

declare(strict_types=1);

/*
 * Chill is a software for social workers
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Chill\Utils\Rector\Rector;

use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Export\DirectExportInterface;
use Chill\MainBundle\Export\ExportInterface;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Export\ListInterface;
use PhpParser\Node;
use Rector\Rector\AbstractRector;
use Rector\Symfony\NodeAnalyzer\ClassAnalyzer;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

final class ChillBundleAddFormDefaultDataOnExportFilterAggregatorRector extends AbstractRector
{
    public function __construct(
        private ClassAnalyzer $classAnalyzer,
    ) {}

    public function getRuleDefinition(): RuleDefinition
    {
        return new RuleDefinition(
            'Add a getFormDefault data method on exports, filters and aggregators, filled with default data',
            [/*
                <<<PHP
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;

class MyClass implements FilterInterface
{
    public function buildForm(FormBuilderInterface $builder)
    {
        $builder->add('foo', PickRollingDateType::class, [
            'label' => 'Test thing',
            'data' => new RollingDate(RollingDate::T_TODAY)
        ]);

        $builder->add('baz', TextType::class, [
            'label' => 'OrNiCar',
            'data' => 'Castor'
        ]);
    }
}
PHP,
<<<PHP

use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;

class MyClass implements FilterInterface
{
    public function buildForm(FormBuilderInterface $builder)
    {
        $builder->add('foo', PickRollingDateType::class, [
            'label' => 'Test thing',
            'data' => new RollingDate(RollingDate::T_TODAY)
        ]);

        $builder->add('baz', TextType::class, [
            'label' => 'OrNiCar',
            'data' => 'Castor'
        ]);
    }
    public function getFormDefaultData(): array
    {
        return ['foo' => new RollingDate(RollingDate::T_TODAY), 'baz' => 'Castor'];
    }
}
PHP */
            ]
        );
    }

    public function getNodeTypes(): array
    {
        return [Node\Stmt\Class_::class];
    }

    public function refactor(Node $node): ?Node
    {
        if (!$node instanceof Node\Stmt\Class_) {
            return null;
        }

        if (
            !$this->classAnalyzer->hasImplements($node, FilterInterface::class)
            && !$this->classAnalyzer->hasImplements($node, AggregatorInterface::class)
            && !$this->classAnalyzer->hasImplements($node, ExportInterface::class)
            && !$this->classAnalyzer->hasImplements($node, DirectExportInterface::class)
            && !$this->classAnalyzer->hasImplements($node, ListInterface::class)
        ) {
            return null;
        }

        $buildFormStmtIndex = null;
        $hasGetFormDefaultDataMethod = false;
        foreach ($node->stmts as $k => $stmt) {
            if (!$stmt instanceof Node\Stmt\ClassMethod) {
                continue;
            }

            if ('buildForm' === $stmt->name->name) {
                $buildFormStmtIndex = $k;
            }

            if ('getFormDefaultData' === $stmt->name->name) {
                $hasGetFormDefaultDataMethod = true;
            }
        }

        if ($hasGetFormDefaultDataMethod || null === $buildFormStmtIndex) {
            return null;
        }

        $stmtBefore = array_slice($node->stmts, 0, $buildFormStmtIndex, false);
        $stmtAfter = array_slice($node->stmts, $buildFormStmtIndex + 1);

        // lines to satisfay phpstan parser
        if (!$node->stmts[$buildFormStmtIndex] instanceof Node\Stmt\ClassMethod) {
            throw new \LogicException();
        }

        ['build_form_method' => $buildFormMethod, 'empty_to_replace' => $emptyToReplace]
            = $this->filterBuildFormMethod($node->stmts[$buildFormStmtIndex], $node);

        $node->stmts = [
            ...$stmtBefore,
            $buildFormMethod,
            $this->makeGetFormDefaultData($node->stmts[$buildFormStmtIndex], $emptyToReplace),
            ...$stmtAfter,
        ];

        return $node;
    }

    private function makeGetFormDefaultData(Node\Stmt\ClassMethod $buildFormMethod, array $emptyToReplace): Node\Stmt\ClassMethod
    {
        $method = new Node\Stmt\ClassMethod('getFormDefaultData');
        $method->flags = Node\Stmt\Class_::MODIFIER_PUBLIC;
        $method->returnType = new Node\Identifier('array');

        $data = new Node\Expr\Array_([]);

        foreach ($emptyToReplace as $key => $value) {
            $item = new Node\Expr\ArrayItem($value, new Node\Scalar\String_($key));
            $data->items[] = $item;
        }

        $method->stmts[] = new Node\Stmt\Return_($data);

        return $method;
    }

    /**
     * @return array{"build_form_method": Node\Stmt\ClassMethod, "empty_to_replace": array<string, mixed>}
     */
    private function filterBuildFormMethod(Node\Stmt\ClassMethod $buildFormMethod, Node\Stmt\Class_ $node): array
    {
        $builderName = $buildFormMethod->params[0]->var->name;

        $newStmts = [];
        $emptyDataToReplace = [];

        foreach ($buildFormMethod->stmts as $stmt) {
            if ($stmt instanceof Node\Stmt\Expression
                // it must be a method call
                && $stmt->expr instanceof Node\Expr\MethodCall
                && false !== ($results = $this->handleMethodCallBuilderAdd($stmt->expr, $builderName, $node))
            ) {
                ['stmt' => $newMethodCAll, 'emptyDataToReplace' => $newEmptyDataToReplace] = $results;
                $newStmts[] = new Node\Stmt\Expression($newMethodCAll);
                $emptyDataToReplace = [...$emptyDataToReplace, ...$newEmptyDataToReplace];
            } else {
                $newStmts[] = $stmt;
            }
        }

        $buildFormMethod->stmts = $newStmts;

        return ['build_form_method' => $buildFormMethod, 'empty_to_replace' => $emptyDataToReplace];
    }

    private function handleMethodCallBuilderAdd(Node\Expr\MethodCall $methodCall, string $builderName, Node\Stmt\Class_ $node): array|false
    {
        $emptyDataToReplace = [];
        // check for chained method call
        if (
            // this means that the MethodCall apply on another method call: a chained
            $methodCall->var instanceof Node\Expr\MethodCall
        ) {
            // as this is chained, we make a recursion on this method

            $resultFormDeepMethodCall = $this->handleMethodCallBuilderAdd($methodCall->var, $builderName, $node);

            if (false === $resultFormDeepMethodCall) {
                return false;
            }

            ['stmt' => $chainedMethodCall, 'emptyDataToReplace' => $newEmptyDataToReplace] = $resultFormDeepMethodCall;
            $emptyDataToReplace = $newEmptyDataToReplace;
            $methodCall->var = $chainedMethodCall;
        }

        if (
            $methodCall->var instanceof Node\Expr\Variable
        ) {
            if ($methodCall->var->name !== $builderName) {
                // ho, this does not apply on a builder, so we cancel all the method calls
                return false;
            }
        }

        if ($methodCall->name instanceof Node\Identifier && 'add' !== $methodCall->name->name) {
            return ['stmt' => $methodCall, 'emptyDataToReplace' => $emptyDataToReplace];
        }

        if (
            // the method call must be "add"
            $methodCall->name instanceof Node\Identifier
            && 'add' === $methodCall->name->name
            // it must have a first argument, a string
            // TODO what happens if a value, or a const ?
            && ($methodCall->args[0] ?? null) instanceof Node\Arg
            && $methodCall->args[0]->value instanceof Node\Scalar\String_
            // and a third argument, an array
            && ($methodCall->args[2] ?? null) instanceof Node\Arg
            && $methodCall->args[2]->value instanceof Node\Expr\Array_
        ) {
            // we parse on the 3rd argument, to find if there is an 'empty_data' key
            $emptyDataIndex = null;
            foreach ($methodCall->args[2]->value->items as $arrayItemIndex => $item) {
                /* @phpstan-ignore-next-line */
                if ('data' === $item->key->value or 'empty_data' === $item->key->value) {
                    $k = $methodCall->args[0]->value->value;
                    $emptyDataToReplace[$k] = $item->value;
                    $emptyDataIndex = $arrayItemIndex;
                }
            }

            if (null !== $emptyDataIndex) {
                $methodCall->args[2]->value->items = array_values(
                    array_filter(
                        $methodCall->args[2]->value->items,
                        /* @phpstan-ignore-next-line */
                        fn (Node\Expr\ArrayItem $item) => 'data' !== $item->key->value
                    )
                );
            }

            return ['stmt' => $methodCall, 'emptyDataToReplace' => $emptyDataToReplace];
        }

        return ['stmt' => $methodCall, 'emptyDataToReplace' => $emptyDataToReplace];
    }
}
