<?php

namespace WordpressModels\DependencyInjection\HookAttributes;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Extension\Extension;
use WordpressModels\DependencyInjection\HookAttributes\Attributes\Action;
use WordpressModels\DependencyInjection\HookAttributes\Attributes\ActivationHook;
use WordpressModels\DependencyInjection\HookAttributes\Attributes\AjaxAction;
use WordpressModels\DependencyInjection\HookAttributes\Attributes\DeactivationHook;
use WordpressModels\DependencyInjection\HookAttributes\Attributes\Filter;
use WordpressModels\DependencyInjection\HookAttributes\Attributes\Hook;

/**
 * Container extension to register hook attributes.
 *
 * Hook attributes {@see Action} and {@see Filter} are registered for autoconfiguration. This means that any service
 * with these attributes will be automatically registered as an action or filter hook.
 *
 * Hook services are lazy loaded, this means that the service is only instantiated when the hook is called.
 *
 * This extension also registers the {@see AjaxAction} attribute for autoconfiguration. This attribute is used to
 * register ajax actions with automatic nonce verification and response wrapping.
 *
 * @package WordpressModels\DependencyInjection\HookAttributes
 */
class HookAttributesExtension extends Extension
{

    public function getNamespace()
    {
        return 'https://code.rtmbusiness.nl/schema/dic/' . $this->getAlias();
    }

    public function getAlias(): string
    {
        return 'wp_hook_attributes';
    }

    public function load(array $configs, ContainerBuilder $container)
    {
        $container->registerAttributeForAutoconfiguration(Action::class, self::addHookTag(...));
        $container->registerAttributeForAutoconfiguration(Filter::class, self::addHookTag(...));

        // ajax action
        $container->registerAttributeForAutoconfiguration(AjaxAction::class, self::addAjaxHookTag(...));

        // activation and deactivation hooks
        $container->registerAttributeForAutoconfiguration(ActivationHook::class, self::addActivationHook(...));
        $container->registerAttributeForAutoconfiguration(DeactivationHook::class, self::addActivationHook(...));

        $container->register(ContainerHookRegistry::class)
            ->setPublic(true);
    }

    public static function addHookTag(Definition $definition, Hook $hook, \ReflectionMethod|\ReflectionClass $reflection)
    {
        // A hook service is lazy when no force_initialize tag is set and the lazy flag is not set
        $lazy = !$definition->hasTag('force_initialize');
        if (!isset($definition->getChanges()['lazy'])) {
            $definition->setLazy($lazy);
        }
        $definition->setPublic(true);
        $injectServices = [];

        // add the hook tag
        if ($reflection instanceof \ReflectionClass) {
            if (!$reflection->hasMethod('__invoke')) {
                throw new \InvalidArgumentException('Class level hooks require the __invoke method (class ' . $reflection->getName() . ')');
            }

            $name = '__invoke';
        } else {
            $name = $reflection->getName();

            // require accepted_args to be set for argument autowiring
            if (!is_null($hook->accepted_args) && $hook->accepted_args < $reflection->getNumberOfParameters()) {
                $injectParameters = array_slice($reflection->getParameters(), $hook->accepted_args);

                foreach ($injectParameters as $parameter) {
                    // try to find services for the parameters
                    $type = $parameter->getType();
                    if ($type && !$type->isBuiltin()) {
                        $injectServices[] = $type->getName();
                    }
                }
            }
        }
        $definition->addTag($hook->type, [
            'method' => $name,
            'hook' => $hook->hook,
            'priority' => $hook->priority,
            'accepted_args' => $hook->accepted_args ?? $reflection->getNumberOfParameters(),
            'when' => $hook->when,
            'admin' => $hook->admin,
            'injectServices' => $injectServices
        ]);
    }

    public static function addAjaxHookTag(Definition $definition, AjaxAction $hook, \ReflectionMethod|\ReflectionClass $reflection)
    {
        // set lazy, not all hooks are always used
        $definition->setLazy(true);
        $definition->setPublic(true);

        // add the hook tag
        if ($reflection instanceof \ReflectionClass) {
            if (!$reflection->hasMethod('__invoke')) {
                throw new \InvalidArgumentException('Class level hooks require the __invoke method (class ' . $reflection->getName() . ')');
            }

            $name = '__invoke';
        } else {
            $name = $reflection->getName();
        }

        $definition->addTag('ajax_action', [
            'method' => $name,
            'action' => $hook->hook,
            'priority' => $hook->priority,
            'nonceId' => $hook->nonceId,
            'nonceKey' => $hook->nonceKey,
            'private' => $hook->private,
            'public' => $hook->public
        ]);
    }

    public static function addActivationHook(Definition $definition, ActivationHook|DeactivationHook $hook, \ReflectionMethod|\ReflectionClass $reflection)
    {
        $class = $reflection instanceof \ReflectionClass ? $reflection : $reflection->getDeclaringClass();
        $classFile = $class->getFileName();
        $file = plugin_basename($classFile);

        self::addHookTag($definition,
            new Action($hook->hook . $file),
            $reflection);
    }

}