<?php

namespace apexl\Io\includes;

use apexl\Io\interfaces\AbstractHook;
use DI\Container;

/**
 * Relatively crude hook system, will need to be improved
 * Class Hook
 * @package apexl\Io\includes
 */
final class HookManager
{

    private const string ALL_CALLBACKS = '_ALL';

    private bool $disableAll = false;
    /**
     * @var array<string, array<callable|class-string<AbstractHook>>>
     */
    private array $registeredCallbacks = [];
    /**
     * @var array<string, array<callable|class-string<AbstractHook>>>
     */
    private array $disabledHooks = [];

    public function __construct(private readonly Container $container) {}

    public function disable(): void
    {
        $this->disableAll = true;
    }

    public function enable(): void
    {
        $this->disableAll = false;
    }

    public function processMany(array $hooks, mixed $input = null, ...$args): mixed
    {
        $hooks = array_filter($hooks);

        $data = $input;
        foreach ($hooks as $hook) {
            $data = $this->processHook($hook, $data, ...$args);
        }

        return $data;
    }


    public function processHook(string $name, mixed $input = null, ...$args): mixed
    {
        if (!$this->canProcess($name)) {
            return $input;
        }

        $output = $input;

        foreach ($this->registeredCallbacks[$name] as $callback) {
            if ($this->canProcess($name, $callback)) {
                $callable = $this->toCallable($callback);
                $parameters = $args;
                if ($output !== null) {
                    $parameters = [$output, ...$parameters];
                }
                if ($result = $this->container->call($callable, $parameters)) {
                    $output = $result;
                }
            }
        }

        return $output;
    }

    private function canProcess(string $name, string|callable|null $callable = null): bool
    {
        return (!$this->isDisabled() && !$this->isHookDisabled($name, $callable) && $this->hasCallback($name));
    }

    private function isDisabled(): bool
    {
        return $this->disableAll;
    }

    public function isHookDisabled(string $name, string|callable|null $callable = null): bool
    {
        $callable = $callable ?? self::ALL_CALLBACKS;

        return in_array($callable, $this->disabledHooks[$name] ?? []);
    }

    private function hasCallback(string $name): bool
    {
        return isset($this->registeredCallbacks[$name]);
    }

    private function toCallable($callback): callable|string
    {
        if (is_callable($callback)) {
            return $callback;
        }

        if (is_string($callback) && class_exists($callback)) {
            return $this->container->get($callback);
        }

        // If the callback is not callable, we will assume that is declared in the 'old' manner
        // of `class:method`, where `method` is a public method of `class`.
        trigger_error(
            'Calling hooks with Class:method is deprecated; Use invokable hooks extending AbstractHook',
            E_USER_DEPRECATED
        );

        return explode(':', $callback);
    }

    public function withoutHook(callable $callWithoutHook, string $name, string|callable|null $callback = null): void
    {
        $this->disableHook($name, $callback);
        $callWithoutHook();
        $this->enableHook($name, $callback);
    }

    public function disableHook(string $name, string|callable|null $callback = null): void
    {
        $callback = $callback ?? self::ALL_CALLBACKS;

        $this->disabledHooks[$name][] = $callback;
    }

    public function enableHook(string $name, string|callable|null $callback = null): void
    {
        if (!isset($this->disabledHooks[$name])) {
            return;
        }

        $callback = $callback ?? self::ALL_CALLBACKS;

        $key = array_search($callback, $this->disabledHooks[$name], true);
        if ($key !== false) {
            unset($this->disabledHooks[$name][$key]);
        }

        if (empty($this->disabledHooks[$name])) {
            unset($this->disabledHooks[$name]);
        }
    }

    public function registerCallback(string $name, string|callable $callback): void
    {
        $this->registeredCallbacks[$name][] = $callback;
    }

}

