<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;

/**
 * @author Alexander M. Turek <me@derrabus.de>
 */
final class AttributeAutoconfigurationPass extends AbstractRecursivePass
{
    protected bool $skipScalars = true;

    private array $classAttributeConfigurators = [];
    private array $methodAttributeConfigurators = [];
    private array $propertyAttributeConfigurators = [];
    private array $parameterAttributeConfigurators = [];

    public function process(ContainerBuilder $container): void
    {
        if (!$container->getAttributeAutoconfigurators()) {
            return;
        }

        foreach ($container->getAttributeAutoconfigurators() as $attributeName => $callables) {
            foreach ($callables as $callable) {
                $callableReflector = new \ReflectionFunction($callable(...));
                if ($callableReflector->getNumberOfParameters() <= 2) {
                    $this->classAttributeConfigurators[$attributeName][] = $callable;
                    continue;
                }

                $reflectorParameter = $callableReflector->getParameters()[2];
                $parameterType = $reflectorParameter->getType();
                $types = [];
                if ($parameterType instanceof \ReflectionUnionType) {
                    foreach ($parameterType->getTypes() as $type) {
                        $types[] = $type->getName();
                    }
                } elseif ($parameterType instanceof \ReflectionNamedType) {
                    $types[] = $parameterType->getName();
                } else {
                    throw new LogicException(\sprintf('Argument "$%s" of attribute autoconfigurator should have a type, use one or more of "\ReflectionClass|\ReflectionMethod|\ReflectionProperty|\ReflectionParameter|\Reflector" in "%s" on line "%d".', $reflectorParameter->getName(), $callableReflector->getFileName(), $callableReflector->getStartLine()));
                }

                foreach (['Class', 'Method', 'Property', 'Parameter'] as $symbol) {
                    if (['Reflector'] === $types || \in_array('Reflection'.$symbol, $types, true)) {
                        $this->{lcfirst($symbol).'AttributeConfigurators'}[$attributeName][] = $callable;
                    }
                }
            }
        }

        $this->container = $container;
        foreach ($container->getDefinitions() as $id => $definition) {
            $this->currentId = $id;
            $this->processValue($definition, true);
        }
    }

    protected function processValue(mixed $value, bool $isRoot = false): mixed
    {
        if (!$value instanceof Definition
            || !$value->isAutoconfigured()
            || ($value->isAbstract() && !$value->hasTag('container.excluded'))
            || $value->hasTag('container.ignore_attributes')
            || !($classReflector = $this->container->getReflectionClass($value->getClass(), false))
        ) {
            return parent::processValue($value, $isRoot);
        }

        $instanceof = $value->getInstanceofConditionals();
        $conditionals = $instanceof[$classReflector->getName()] ?? new ChildDefinition('');

        $this->callConfigurators($this->classAttributeConfigurators, $conditionals, $classReflector);

        if ($this->parameterAttributeConfigurators) {
            try {
                $constructorReflector = $this->getConstructor($value, false);
            } catch (RuntimeException) {
                $constructorReflector = null;
            }

            if ($constructorReflector) {
                foreach ($constructorReflector->getParameters() as $parameterReflector) {
                    $this->callConfigurators($this->parameterAttributeConfigurators, $conditionals, $parameterReflector);
                }
            }
        }

        if ($this->methodAttributeConfigurators || $this->parameterAttributeConfigurators) {
            foreach ($classReflector->getMethods(\ReflectionMethod::IS_PUBLIC) as $methodReflector) {
                if ($methodReflector->isConstructor() || $methodReflector->isDestructor()) {
                    continue;
                }

                $this->callConfigurators($this->methodAttributeConfigurators, $conditionals, $methodReflector);

                foreach ($methodReflector->getParameters() as $parameterReflector) {
                    $this->callConfigurators($this->parameterAttributeConfigurators, $conditionals, $parameterReflector);
                }
            }
        }

        if ($this->propertyAttributeConfigurators) {
            foreach ($classReflector->getProperties(\ReflectionProperty::IS_PUBLIC) as $propertyReflector) {
                if ($propertyReflector->isStatic()) {
                    continue;
                }

                $this->callConfigurators($this->propertyAttributeConfigurators, $conditionals, $propertyReflector);
            }
        }

        if (!isset($instanceof[$classReflector->getName()]) && new ChildDefinition('') != $conditionals) {
            $instanceof[$classReflector->getName()] = $conditionals;
            $value->setInstanceofConditionals($instanceof);
        }

        return parent::processValue($value, $isRoot);
    }

    /**
     * Call all the configurators for the given attribute.
     *
     * @param array<class-string, callable[]> $configurators
     */
    private function callConfigurators(array &$configurators, ChildDefinition $conditionals, \ReflectionClass|\ReflectionMethod|\ReflectionParameter|\ReflectionProperty $reflector): void
    {
        if (!$configurators) {
            return;
        }

        foreach ($reflector->getAttributes() as $attribute) {
            foreach ($this->findConfigurators($configurators, $attribute->getName()) as $configurator) {
                $configurator($conditionals, $attribute->newInstance(), $reflector);
            }
        }
    }

    /**
     * Find the first configurator for the given attribute name, looking up the class hierarchy.
     */
    private function findConfigurators(array &$configurators, string $attributeName): array
    {
        if (\array_key_exists($attributeName, $configurators)) {
            return $configurators[$attributeName];
        }

        if (class_exists($attributeName) && $parent = get_parent_class($attributeName)) {
            return $configurators[$attributeName] = $this->findConfigurators($configurators, $parent);
        }

        return $configurators[$attributeName] = [];
    }
}
