<?php

/**
 * @file classes/plugins/Hook.php
 *
 * Copyright (c) 2014-2021 Simon Fraser University
 * Copyright (c) 2000-2021 John Willinsky
 * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
 *
 * @class Hook
 *
 * @ingroup plugins
 *
 * @brief Class implementing a registry of hooks for extending core functionality
 */

namespace PKP\plugins;

use PKP\core\Registry;

class Hook
{
    public const SEQUENCE_CORE = 0;
    public const SEQUENCE_NORMAL = 256;
    public const SEQUENCE_LATE = 512;
    public const SEQUENCE_LAST = 768;

    public const CONTINUE = false;
    public const ABORT = true;

    /**
     * Get the current set of hook registrations.
     *
     * @param string $hookName Name of hook to optionally return
     *
     * @return mixed Array of all hooks or just those attached to $hookName, or
     *   null if nothing has been attached to $hookName
     */
    public static function &getHooks(?string $hookName = null): ?array
    {
        $hooks = & Registry::get('hooks', true, []);
        if ($hookName === null) {
            return $hooks;
        }

        if (isset($hooks[$hookName])) {
            return $hooks[$hookName];
        }

        $returner = null;
        return $returner;
    }

    /**
     * Clear hooks registered against the given name.
     */
    public static function clear(string $hookName): void
    {
        $hooks = & static::getHooks();
        unset($hooks[$hookName]);
    }

    /**
     * Register a hook against the given hook name.
     *
     * @param string $hookName Name of hook to register against
     * @param object|array $callback Callback pseudo-type
     * @param int $hookSequence Optional hook sequence specifier SEQUENCE_...
     */
    public static function add(string $hookName, callable $callback, int $hookSequence = self::SEQUENCE_NORMAL): void
    {
        $hooks = & static::getHooks();
        $hooks[$hookName][$hookSequence][] = $callback;
    }

    /**
     * Alias of Hook::add
     *
     * @deprecated 3.4
     */
    public static function register(string $hookName, callable $callback, int $hookSequence = self::SEQUENCE_NORMAL): void
    {
        self::add($hookName, $callback, $hookSequence);
    }

    /**
     * Call each callback registered against $hookName in sequence.
     * The first callback that returns ABORT will interrupt processing and
     * this function will return ABORT; otherwise, all callbacks will be
     * called in sequence and the return value of this call will be
     * CONTINUE.
     *
     * The signature of the callback function should be:
     *   function callback($hookName, $args) : bool;
     * ...and $args will receive the array provided here.
     *
     * This function should be considered deprecated in favour of
     * Hook::call.
     */
    public static function call(string $hookName, array $args = []): mixed
    {
        // Called only by Unit Test
        // This behaviour is DEPRECATED and not replicated in the preferred
        // Hook::call function.
        if (self::rememberCalledHooks(true)) {
            // Remember the called hooks for testing.
            $calledHooks = & static::getCalledHooks();
            $calledHooks[] = [
                $hookName, $args
            ];
        }

        return self::run($hookName, [$args]);
    }

    /**
     * Call each callback registered against $hookName in sequence.
     * The first callback that returns ABORT will interrupt processing and
     * this function will return ABORT; otherwise, all callbacks will be
     * called in sequence and the return value of this call will be
     * CONTINUE.
     *
     * The signature of a callback function should be:
     *   function callback($hookName, ...) : bool;
     * where ... corresponds to the parameters named/listed in the $args
     * parameter to Hook::call. These may be named if desired,
     * and may include references.
     */
    public static function run(string $hookName, $args): bool
    {
        $hooks = & static::getHooks();
        if (!isset($hooks[$hookName])) {
            return self::CONTINUE;
        }

        ksort($hooks[$hookName], SORT_NUMERIC);
        foreach ($hooks[$hookName] as $priority => $hookList) {
            foreach ($hookList as $callback) {
                if (call_user_func_array($callback, array_merge([$hookName], $args)) === self::ABORT) {
                    return self::ABORT;
                }
            }
        }

        return self::CONTINUE;
    }


    //
    // Methods required for testing only.
    //
    /**
     * Set/query the flag that triggers storing of
     * called hooks.
     *
     * @param bool $askOnly When set to true, the flag will not
     *   be changed but only returned.
     * @param bool $updateTo When $askOnly is set to 'true' then
     *   this parameter defines the value of the flag.
     *
     * @return bool The current value of the flag.
     */
    public static function rememberCalledHooks(bool $askOnly = false, bool $updateTo = true): bool
    {
        static $rememberCalledHooks = false;
        if (!$askOnly) {
            $rememberCalledHooks = $updateTo;
        }
        return $rememberCalledHooks;
    }

    /**
     * Switch off the function to store hooks and delete all stored hooks.
     * Always call this after using otherwise we get a severe memory.
     *
     * @param bool $leaveAlive Set this to true if you only want to
     *   delete hooks stored so far but if you want to record future
     *   hook calls, too.
     */
    public static function resetCalledHooks(bool $leaveAlive = false): void
    {
        if (!$leaveAlive) {
            static::rememberCalledHooks(false, false);
        }
        $calledHooks = & static::getCalledHooks();
        $calledHooks = [];
    }

    /**
     * Return a reference to the stored hooks.
     */
    public static function &getCalledHooks(): array
    {
        static $calledHooks = [];
        return $calledHooks;
    }
}

if (!PKP_STRICT_MODE) {
    class_alias('\PKP\plugins\Hook', '\HookRegistry');
    foreach (['SEQUENCE_CORE', 'SEQUENCE_NORMAL', 'SEQUENCE_LATE', 'SEQUENCE_LAST'] as $constantName) {
        define('HOOK_' . $constantName, constant('\HookRegistry::' . $constantName));
    }
}
