<?php 
/**
 * @package    Proxim
 * @author     Dominic Tanui <dominic@codeworldweb.com | https://codeworldweb.com>
 * @copyright  2019 Proxim
 * @version    1.5.0
 * @since      File available since Release 1.0.0
 */

namespace Proxim;

use Db;
use Exception;
use Proxim\Adapter\SymfonyContainer;
use Proxim\Database\DbQuery;
use Proxim\Cache\Cache;
use Proxim\Module\Module;
use Proxim\ObjectModel;
use Proxim\Site\Site;

/**
 * Class Hook.
 */
class Hook extends ObjectModel
{
    /**
     * @var string Hook name identifier
     */
    public $name;

    /**
     * @var string Hook title (displayed in BO)
     */
    public $title;

    /**
     * @var string Hook description
     */
    public $description;

    /**
     * @var bool
     */
    public $position = false;

    /**
     * @var array List of executed hooks on this page
     */
    public static $executed_hooks = array();

    public static $native_module;

    /**
     * @see ObjectModel::$definition
     */
    public static $definition = array(
        'table' => 'hook',
        'primary' => 'hook_id',
        'fields' => array(
            'name' => array('type' => self::TYPE_STRING, 'validate' => 'isHookName', 'required' => true, 'size' => 64),
            'title' => array('type' => self::TYPE_STRING, 'validate' => 'isGenericName'),
            'description' => array('type' => self::TYPE_HTML, 'validate' => 'isCleanHtml'),
            'position' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'),
        ),
    );

    const MODULE_LIST_BY_HOOK_KEY = 'hook_module_exec_list_';

    public function add($autodate = true, $null_values = false)
    {
        Cache::clean('hook_idsbyname');

        return parent::add($autodate, $null_values);
    }

    public static function normalizeHookName($hookName)
    {
        if (strtolower($hookName) == 'displayheader') {
            return 'displayHeader';
        }

        return $hookName;
    }

    public static function isDisplayHookName($hook_name)
    {
        $hook_name = strtolower(self::normalizeHookName($hook_name));
        return strpos($hook_name, 'display') === 0;
    }

    public static function registerHook($module_instance, $hook_name)
    {
        $return = true;
        if (is_array($hook_name)) {
            $hook_names = $hook_name;
        } else {
            $hook_names = array($hook_name);
        }

        foreach ($hook_names as $hook_name) {
            // Check hook name validation and if module is installed
            if (!Validate::isHookName($hook_name)) {
                throw new Exception('Invalid hook name');
            }
            if (!isset($module_instance->id) || !is_numeric($module_instance->id)) {
                return false;
            }

            Hook::exec('actionModuleRegisterHookBefore', array('object' => $module_instance, 'hook_name' => $hook_name));
            // Get hook id
            $hook_id = Hook::getIdByName($hook_name);

            // If hook does not exist, we create it
            if (!$hook_id) {
                $new_hook = new Hook();
                $new_hook->name = pSQL($hook_name);
                $new_hook->title = pSQL($hook_name);
                $new_hook->position = 1;
                $new_hook->add();
                $hook_id = $new_hook->id;
                if (!$hook_id) {
                    return false;
                }
            }

            // Get module position in hook
            $sql = 'SELECT MAX(`position`) AS position FROM ' . Db::prefix('hook_module') . ' WHERE `hook_id` = ' . (int) $hook_id;
            if (!$position = Db::getInstance()->getValue($sql)) {
                $position = 0;
            }

            // Register module in hook
            $return &= Db::getInstance()->insert('hook_module', array(
                'module_id' => (int) $module_instance->id,
                'hook_id' => (int) $hook_id,
                'position' => (int) ($position + 1),
            ));

            Hook::exec('actionModuleRegisterHookAfter', array('object' => $module_instance, 'hook_name' => $hook_name));
        }

        return $return;
    }

    public static function unregisterHook($module_instance, $hook_name, $shop_list = null)
    {
        if (is_numeric($hook_name)) {
            // $hook_name passed it the hook_id
            $hook_id = $hook_name;
            $hook_name = Hook::getNameById((int) $hook_id);
        } else {
            $hook_id = Hook::getIdByName($hook_name);
        }

        if (!$hook_id) {
            return false;
        }

        Hook::exec('actionModuleUnRegisterHookBefore', array('object' => $module_instance, 'hook_name' => $hook_name));

        // Unregister module on hook by id
        $result = Db::getInstance()->execute(
            'DELETE FROM ' . Db::prefix('hook_module') . ' WHERE `module_id` = ' . (int) $module_instance->id . ' AND `hook_id` = ' . (int) $hook_id
        );

        Hook::exec('actionModuleUnRegisterHookAfter', array('object' => $module_instance, 'hook_name' => $hook_name));

        return $result;
    }

    /**
     * Return hook ID from name.
     *
     * @param string $hook_name Hook name
     *
     * @return int Hook ID
     */
    public static function getIdByName($hook_name)
    {
        $hook_name = strtolower($hook_name);
        if (!Validate::isHookName($hook_name)) {
            return false;
        }

        $cache_id = 'hook_idsbyname';
        if (!Cache::isStored($cache_id)) {
            // Get all hook ID by name and alias
            $hook_ids = array();
            $db = Db::getInstance();
            $result = $db->executeS('
			SELECT `hook_id`, `name`
			FROM ' . Db::prefix('hook') . '
			UNION
			SELECT `hook_id`, ha.`alias` as name
			FROM ' . Db::prefix('hook_alias') . ' ha
			INNER JOIN ' . Db::prefix('hook') . ' h ON ha.name = h.name', false);
            while ($row = $db->nextRow($result)) {
                $hook_ids[strtolower($row['name'])] = $row['hook_id'];
            }
            Cache::store($cache_id, $hook_ids);
        } else {
            $hook_ids = Cache::retrieve($cache_id);
        }

        return isset($hook_ids[$hook_name]) ? $hook_ids[$hook_name] : false;
    }

    /**
     * Return hook ID from name.
     */
    public static function getNameById($hook_id)
    {
        $cache_id = 'hook_namebyid_' . $hook_id;
        if (!Cache::isStored($cache_id)) {
            $result = Db::getInstance()->getValue('
                SELECT `name`
                FROM ' . Db::prefix('hook') . '
                WHERE `hook_id` = ' . (int) $hook_id
            );
            Cache::store($cache_id, $result);

            return $result;
        }

        return Cache::retrieve($cache_id);
    }

    /**
     * Get list of modules we can execute per hook.
     *
     * @since 1.5.0
     *
     * @param string $hook_name Get list of modules for this hook if given
     *
     * @return array
     */
    public static function getHookModuleExecList($hook_name = null)
    {
        $application = Application::getInstance();
        $site = $application->container->site;

        $cache_id = self::MODULE_LIST_BY_HOOK_KEY . (isset($site->id) ? '_' . $site->id : '');
        if (!Cache::isStored($cache_id)) {
            // SQL Request
            $sql = new DbQuery();
            $sql->select('h.`name` as hook, m.`module_id`, h.`hook_id`, m.`name` as module');
            $sql->from('module', 'm');
            $sql->innerJoin('hook_module', 'hm', 'hm.`module_id` = m.`module_id`');
            $sql->innerJoin('hook', 'h', 'hm.`hook_id` = h.`hook_id`');

            if ($site && Validate::isLoadedObject($site->id)) {
                $sql->where('hm.`site_id` = ' . (int) $site->id);
            }

            $sql->groupBy('hm.hook_id, hm.module_id');
            $sql->orderBy('hm.`position`');

            $list = array();
            if ($result = Db::getInstance(PROX_USE_SQL_SLAVE)->executeS($sql)) {
                foreach ($result as $row) {
                    $row['hook'] = strtolower($row['hook']);
                    if (!isset($list[$row['hook']])) {
                        $list[$row['hook']] = array();
                    }

                    $list[$row['hook']][] = array(
                        'hook_id' => $row['hook_id'],
                        'module' => $row['module'],
                        'module_id' => $row['module_id'],
                    );
                }
            }
            if ($hook_name != 'displayPayment' && $hook_name != 'displayPaymentEU' && $hook_name != 'paymentOptions' && $hook_name != 'displayBackOfficeHeader') {
                Cache::store($cache_id, $list);
            }
        } else {
            $list = Cache::retrieve($cache_id);
        }

        // If hook_name is given, just get list of modules for this hook
        if ($hook_name) {
            $hook_name = strtolower($hook_name);

            $return = array();
            $inserted_modules = array();
            if (isset($list[$hook_name])) {
                $return = $list[$hook_name];
            }
            foreach ($return as $module) {
                $inserted_modules[] = $module['module_id'];
            }

            return count($return) > 0 ? $return : false;
        } else {
            return $list;
        }
    }

    /**
     * Get the list of hook aliases.
     *
     * @since 1.7.1.0
     *
     * @return array
     */
    private static function getHookAliasesList()
    {
        $cacheId = 'hook_aliases';
        if (!Cache::isStored($cacheId)) {
            $hookAliasList = Db::getInstance()->executeS('SELECT * FROM '. Db::prefix('hook_alias'));
            $hookAliases = array();
            if ($hookAliasList) {
                foreach ($hookAliasList as $ha) {
                    if (!isset($hookAliases[$ha['name']])) {
                        $hookAliases[$ha['name']] = array();
                    }
                    $hookAliases[$ha['name']][] = $ha['alias'];
                }
            }
            Cache::store($cacheId, $hookAliases);

            return $hookAliases;
        }

        return Cache::retrieve($cacheId);
    }

    /**
     * Return backward compatibility hook names.
     *
     * @since 1.7.1.0
     *
     * @param $hookName
     *
     * @return array
     */
    private static function getHookAliasesFor($hookName)
    {
        $cacheId = 'hook_aliases_' . $hookName;
        if (!Cache::isStored($cacheId)) {
            $aliasesList = Hook::getHookAliasesList();

            if (isset($aliasesList[$hookName])) {
                Cache::store($cacheId, $aliasesList[$hookName]);

                return $aliasesList[$hookName];
            }

            $retroName = array_keys(array_filter($aliasesList, function ($elem) use ($hookName) {
                return in_array($hookName, $elem);
            }));

            if (empty($retroName)) {
                Cache::store($cacheId, array());

                return array();
            }

            Cache::store($cacheId, $retroName);

            return $retroName;
        }

        return Cache::retrieve($cacheId);
    }

    /**
     * Check if a hook or one of its old names is callable on a module.
     *
     * @since 1.7.1.0
     *
     * @param $module
     * @param $hookName
     *
     * @return bool
     */
    private static function isHookCallableOn($module, $hookName)
    {
        $aliases = Hook::getHookAliasesFor($hookName);
        $aliases[] = $hookName;

        return array_reduce($aliases, function ($prev, $curr) use ($module) {
            return $prev || is_callable(array($module, 'hook' . $curr));
        }, false);
    }

    /**
     * Call a hook (or one of its old name) on a module.
     *
     * @since 1.7.1.0
     *
     * @param $module
     * @param $hookName
     * @param $hookArgs
     *
     * @return string
     */
    private static function callHookOn($module, $hookName, $hookArgs)
    {
        if (is_callable(array($module, 'hook' . $hookName))) {
            return Hook::coreCallHook($module, 'hook' . $hookName, $hookArgs);
        }
        foreach (Hook::getHookAliasesFor($hookName) as $hook) {
            if (is_callable(array($module, 'hook' . $hook))) {
                return Hook::coreCallHook($module, 'hook' . $hook, $hookArgs);
            }
        }

        return '';
    }

    /**
     * Execute modules for specified hook.
     *
     * @param string $hook_name Hook Name
     * @param array $hook_args Parameters for the functions
     * @param int $module_id Execute hook for this module only
     * @param bool $array_return If specified, module output will be set by name in an array
     * @param int $site_id If specified, hook will be execute the site with this ID
     * @param bool $chain If specified, hook will chain the return of hook module
     *
     * @throws Exception
     *
     * @return string/array modules output
     */
    public static function exec(
        $hook_name,
        $hook_args = array(),
        $module_id = null,
        $array_return = false,
        $site_id = null,
        $chain = false
    ) {
        $hookRegistry = self::getHookRegistry();
        $isRegistryEnabled = null !== $hookRegistry;

        if ($isRegistryEnabled) {
            $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
            $hookRegistry->selectHook($hook_name, $hook_args, $backtrace[0]['file'], $backtrace[0]['line']);
        }

        // $chain & $array_return are incompatible so if chained is set to true, we disable the array_return option
        if (true === $chain) {
            $array_return = false;
        }

        // Check arguments validity
        if (($module_id && !is_numeric($module_id)) || !Validate::isHookName($hook_name)) {
            throw new Exception('Invalid module_id or hook_name');
        }

        // If no modules associated to hook_name we stop the function
        if (!$module_list = Hook::getHookModuleExecList($hook_name)) {
            if ($isRegistryEnabled) {
                $hookRegistry->collect();
            }
            if ($array_return) {
                return array();
            } else {
                return '';
            }
        }

        // Check if hook exists
        if (!$hook_id = Hook::getIdByName($hook_name)) {
            if ($isRegistryEnabled) {
                $hookRegistry->collect();
            }
            if ($array_return) {
                return array();
            } else {
                return false;
            }
        }

        // Store list of executed hooks on this page
        Hook::$executed_hooks[$hook_id] = $hook_name;

        $application = Application::getInstance();
        if (!isset($hook_args['cookie']) || !$hook_args['cookie']) {
            $hook_args['cookie'] = $application->container->cookie;
        }

        // Look on modules list
        $altern = 0;
        if ($array_return) {
            $output = array();
        } else {
            $output = '';
        }

        $different_site = false;
        if ($site_id !== null && Validate::isUnsignedId($site_id) && $site_id != $application->container->site->id) {
            $old_site = clone $application->site;
            $site = new Site((int) $site_id);
            if (Validate::isLoadedObject($site)) {
                $application->site = $site;
                $different_site = true;
            }
        }

        foreach ($module_list as $key => $array) {
            // Check errors
            if ($module_id && $module_id != $array['module_id']) {
                continue;
            }

            if (!($moduleInstance = Module::getInstanceByName($array['module']))) {
                continue;
            }

            if ($isRegistryEnabled) {
                $hookRegistry->hookedByModule($moduleInstance);
            }

            if (Hook::isHookCallableOn($moduleInstance, $hook_name)) {
                $hook_args['altern'] = ++$altern;

                if (0 !== $key && true === $chain) {
                    $hook_args = $output;
                }

                $display = Hook::callHookOn($moduleInstance, $hook_name, $hook_args);

                if ($array_return) {
                    $output[$moduleInstance->name] = $display;
                } else {
                    if (true === $chain) {
                        $output = $display;
                    } else {
                        $output .= $display;
                    }
                }
                if ($isRegistryEnabled) {
                    $hookRegistry->hookedByCallback($moduleInstance, $hook_args);
                }
            }
        }

        if ($different_site) {
            $application->site = $old_site;
        }

        if (true === $chain) {
            if (isset($output['cookie'])) {
                unset($output['cookie']);
            }
            if (isset($output['cart'])) {
                unset($output['cart']);
            }
        }

        if ($isRegistryEnabled) {
            $hookRegistry->hookWasCalled();
            $hookRegistry->collect();
        }

        return $output;
    }

    public static function coreCallHook($module, $method, $params)
    {
        return $module->{$method}($params);
    }

    public static function coreRenderWidget($module, $hook_name, $params)
    {
        return $module->renderWidget($hook_name, $params);
    }

    /**
     * @return \HookRegistry|null
     */
    private static function getHookRegistry()
    {
        $sfContainer = SymfonyContainer::getInstance();
        if (null !== $sfContainer && 'dev' === $sfContainer->getParameter('kernel.environment')) {
            return $sfContainer->get('proxim.hooks_registry');
        }

        return null;
    }
}