<?php

/**
 * Le namespace auxquels tous les modules doivent appartenir (ainsi que ce manager)
 */
namespace Module;

require_once __DIR__."/utils.class.php";
require_once __DIR__."/lien_module.class.php";
use \application;
use \om_dbform;
use \lien_module;
use \RuntimeException;

class module_manager
{
    /**
     * Instance du framework openmairie.
     *
     * @var application
     */
    public $framework;

    /**
     * Enregistre l'état du chargement de la configuration dynamique des modules
     *
     * @var bool
     */
    public $modules_dyn_conf_loaded = false;

    /**
     * Liste de tous les modules installés/disponibles
     *
     * La variable vaut 'null' tant qu'elle n'a pas été initialisée, puis un 'array' ensuite.
     *
     * @var array|null
     */
    public $all_available_modules = null;

    /**
     * Liste des déclencheurs (hooks) disponibles/reconnus
     *
     * @var array
     */
    public $all_available_hooks = array();

    /**
     * Liste des modules chargés pour chaque objet/classe
     *
     * @var array
     */
    public $modules_by_object = array();

    /**
     * Si 'true' (pour une clé donnée, c'est à dire: une classe d'objet) alors la fonction
     * 'load_modules' n'aura aucun effet.
     *
     * Utilisé par la fonction homonyme 'enabled_for()';
     */
    protected $enabled_for = array();

    /**
     * Renvoie le chemin vers le fichier de configuration dynamique des modules
     *
     * @return string
     */
    public function get_modules_dyn_conf_path() {
        return __DIR__.'/../dyn/modules.inc.php';
    }

    /**
     * Retourne une nouvelle instance, avec injection de dépendances.
     *
     * @param  application  $framework  Une instance du framework openmairie
     *
     * @return module_manager
     */
    public function __construct(application $framework) {
        $this->framework = $framework;
        $this->log(__METHOD__, '');
    }

    /**
     * Log un message en lui ajoutant un préfixe.
     *
     * @param  string  $method   La méthode d'où provient le message (càd: __METHOD__)
     * @param  string  $message  Le message à écire dans le ficheir de logs
     * @param  string  $level    Le niveau de log
     *
     * @return void
     */
    protected function log(string $method, string $message, string $level = 'DEBUG') {

        $namespace_escaped = str_replace('\\', '\\\\', __NAMESPACE__);
        $method_short = preg_replace('/^'.$namespace_escaped.'\\\\/', '', $method);

        $this->framework->log($method_short, $message, $level);
        //$this->framework->log($method_short, $message, $level, 'modules');
    }

    /**
     * Fonction qui instancie le module nommée 
     * 
     * @param string $module_name : nom du module à instancier
     * @param  lien_module  $wf_module_link  Instance de lien module
     * @param  mixed        $object          Un objet métier à associer au module
     * 
     * @return object : Instance du module
     */
    public function get_module_instance($module_name, lien_module $wf_module_link = null, $object = null) {
        $module_dir = $this->get_modules_dir();
        $module_script = "$module_dir/$module_name.php";
        $this->log(__METHOD__, "script '$module_script'");

        if (! is_file($module_script)) {
            $module_dir = dirname($module_script).'/'.basename($module_script, '.php');
            if (! is_dir($module_dir)) return null;
            $module_script = "$module_dir/$module_name.php";
            if (! is_file($module_script)) return null;
        }

        $this->log(__METHOD__, "module: loading module '$module_script'");
        $module_name = basename($module_script, '.php');
        if (! empty($wf_module_link)) {
            $this->log(__METHOD__, "loading module '$module_name' (id:".$wf_module_link->getVal($wf_module_link->clePrimaire).", ordre:".$wf_module_link->getVal('ordre').")");
        } else {
            $this->log(__METHOD__, "loading module '$module_name' (no object link instance)");
        }
        require_once $module_script;
        $module_name_camel = str_replace(' ', '', ucfirst(preg_replace('/[^a-zA-Z0-9]/', ' ', $module_name)));
        $module_namespace = "Module\\$module_name_camel";
        $module_class = $module_name;
        $module_class_path = $module_namespace.'\\'.$module_class;
        $this->log(__METHOD__, "module: module classpath '$module_class_path'");
        $module_instance = new $module_class_path($module_dir, $this->framework, $wf_module_link, $object);
        $this->log(__METHOD__, "module: loaded module '$module_class_path'");
        if (! empty($wf_module_link)) {
            $module_instance->set_parameters($wf_module_link->getVal('parametres'), 'ini');
        }
        return $module_instance;
    }

    /**
     * Fonction permettant de recuperer un module nommée et de lancer ses fonctions d'installations
     *
     * @param string $module_name : nom du module à instancier
     *
     * @return boolean : True ou False
     */
    public function install_modules($module_names) {
        foreach ($module_names as $module_name){
            try {
                $module_instance = $this->get_module_instance($module_name);
                $module_instance->install();
            } catch (RuntimeException $re) {
                $this->log(__METHOD__, " RuntimeException:  $module_name => $re");
                file_put_contents(STDERR, " RuntimeException: $module_name => $re", FILE_APPEND );
                return false;
            }
        }
        return true;
    }

    /**
     * Fonction permettant de recuperer un module nommée et de lancer ses fonctions d'installations
     *
     * @param string $module_name : nom du module à instancier
     *
     * @return boolean : True ou False
     */
    public function uninstall_modules($module_names) {
        foreach ($module_names as $module_name){
            try {
                $module_instance = $this->get_module_instance($module_name);
                $module_instance->uninstall();
            } catch (RuntimeException $re) {
                $this->log(__METHOD__, " RuntimeException:  $module_name => $re");
                file_put_contents(STDERR, " RuntimeException: $module_name => $re", FILE_APPEND );
                return false;
            }
        }
        return true;
    }

    /**
     * Charge la configuration dynamique des modules (si pas déjà fait), spécifiquement:
     * - défini '$this->all_available_hooks' à partir de '$hooks'
     * - défini '$this->enabled_for' à partir de '$modules_enabled_for'
     * - défini '$this->modules_dir' à partir de '$modules_dir'
     *
     * Si la variable '$this->modules_dyn_conf_loaded' vaut 'true' alors ne fait rien.
     *
     * @return void
     */
    public function load_modules_dyn_conf() {
        if (! $this->modules_dyn_conf_loaded) {
            $dyn_conf = $this->get_modules_dyn_conf_path();
            if (file_exists($dyn_conf)) {
                require $dyn_conf;
                if (! isset($hooks)) {
                    throw new RuntimeException(
                        "Variable '\$hooks' is not defined in '$dyn_conf'");
                }
                if (! isset($modules_enabled_for)) {
                    throw new RuntimeException(
                        "Variable '\$modules_enabled_for' is not defined in '$dyn_conf'");
                }
                if (! isset($modules_dir)) {
                    throw new RuntimeException(
                        "Variable '\$modules_dir' is not defined in '$dyn_conf'");
                }
                if (empty($modules_dir) && ! empty($modules_enabled_for)) {
                    throw new RuntimeException(
                        "Variable '\$modules_dir' is empty but '\$modules_enabled_for' isn't ".
                        "in '$dyn_conf' (fill both, or empty both)");
                }
                $this->all_available_hooks = $hooks;
                $this->enabled_for = $modules_enabled_for;
                $this->modules_dir = $modules_dir;
            }
            $this->modules_dyn_conf_loaded = true;
        }
    }

    /**
     * Renvoi la liste des déclencheurs (hooks) disponibles.
     * Format:
     *   'nom technique du déclencheur' => "Description métier du déclencheur"
     *
     * @return array
     */
    public function get_all_available_hooks() {
        $this->load_modules_dyn_conf();
        return $this->all_available_hooks;
    }

    /**
     * Renvoie 'true' si les modules sont activés pour cet objet.
     *
     * C'est utile pour empêcher le déclenchement infini (notament lorsqu'un objet s'initialise
     * et en charge d'autre) mais aussi pour éviter que 100% des objets essaient de charger
     * leurs modules, ce qui ralentirait significativement l'application.
     *
     * Il est donc préférable de déclarer manuellement les objets pour lesques les modules sont
     * pertinents et peuvent être activés (généralement des objets métiers spécifiques).
     *
     * @param  om_dbform  $object  Objet métier concerné
     *
     * @return bool
     */
    public function enabled_for(string $object_name) {
        $this->load_modules_dyn_conf();
        return in_array($object_name, $this->enabled_for);
    }

    /**
     * Renvoi le chemin absolu vers le dossier racines des modules
     *
     * @return string
     */
    public function get_modules_dir() {
        $this->load_modules_dyn_conf();
        return $this->modules_dir;
    }

    /**
     * Instancie/retourne tous les modules installés/disponibles
     */
    public function get_all_available_modules() {
        if (is_null($this->all_available_modules)) {
            $this->all_available_modules = array();
            foreach(glob($this->get_modules_dir().'/*') as $file) {
                $this->log(__METHOD__, "module: file or diretory '$file'");
                $module_script = $file;
                if (is_dir($file)) {
                    $module_dir = $file;
                    $module_name = basename($module_dir);
                    $module_script = "$module_dir/$module_name.php";
                }
                elseif (is_file($file)) {
                    $file_ext = pathinfo($file, PATHINFO_EXTENSION);
                    $this->log(__METHOD__, "module: file extension '$file_ext'");
                    if ($file_ext != 'php') continue;
                    $file_ext_2nd = pathinfo(dirname($file).'/'.basename($file, '.php'), PATHINFO_EXTENSION);
                    $this->log(__METHOD__, "module: file 2nd extension '$file_ext_2nd'");
                    if ($file_ext_2nd == 'conf') continue;
                }

                if (is_file($module_script)) {
                    $module_dir = dirname($module_script);
                    $this->log(__METHOD__, "module: loading module '$module_script'");
                    require_once $module_script;
                    $module_name = basename($module_script, '.php');
                    $module_name_camel = str_replace(' ', '', ucfirst(preg_replace('/[^a-zA-Z0-9]/', ' ', $module_name)));
                    $module_namespace = "Module\\$module_name_camel";
                    $module_class = $module_name;
                    $module_class_path = $module_namespace.'\\'.$module_class;
                    $this->log(__METHOD__, "module: module classpath '$module_class_path'");
                    $module_instance = new $module_class_path($module_dir, $this->framework);
                    $this->all_available_modules[] = $module_instance;
                }
            }
        }
        return $this->all_available_modules;
    }

    /**
     * Renvoie le classpath d'un module à partir de son nom.
     *
     * @param  string  $module_name  Le nom du module dont on veut le classpath
     *
     * @return string
     */
    public function get_module_classpath(string $module_name) {
        $module_name_camel = str_replace(' ', '', ucfirst(
            preg_replace('/[^a-zA-Z0-9]/', ' ', $module_name)));
        $module_namespace = "Module\\$module_name_camel";
        $module_class = $module_name;
        $module_class_path = $module_namespace.'\\'.$module_class;
        return $module_class_path;
    }

    /**
     * Instancie un module à partir de son nom.
     *
     * @param  string  $module_name  Le nom du module à instancier
     *
     * @return module|null  'null' si module non trouvé
     */
    public function get_new_module_by_name(string $module_name) {
        $module_dir = $this->get_modules_dir()."/$module_name";
        $module_script = $module_dir."/$module_name.php";
        if (is_file($module_script)) {
            require_once $module_script;
            $module_class_path = $this->get_module_classpath($module_name);
            $this->log(__METHOD__, "module: module classpath '$module_class_path'");
            return new $module_class_path($this->framework);
        }
        return null;
    }

    /**
     * Exécute la méthode d'installation du module spécifié.
     *
     * @param  string  $module_name  Le nom du module à installer
     *
     * @return bool  'true' si l'installation s'est bien passée, 'false' sinon
     */
    public function install(string $module_name) {
        $module = $this->get_new_module_by_name($module_name);
        if (! empty($module)) {
            return $module->install();
        }
        return false;
    }

    /**
     * Exécute la méthode de désinstallation du module spécifié.
     *
     * @param  string  $module_name  Le nom du module à désinstaller
     *
     * @return bool  'true' si la désinstallation s'est bien passée, 'false' sinon
     */
    public function uninstall(string $module_name) {
        $module = $this->get_new_module_by_name($module_name);
        if (! empty($module)) {
            return $module->uninstall();
        }
        return false;
    }


    /**
     * Code générique pour l'éxécution des trigger spécifié, pour chaque module 
     *
     * @param     string  $hook          Le nom du hook
     * @param     array   $modules       table des modules appler
     * @param     string  $method_prefix Préfix pour le hook, basée sur le nom de la classe
     * @param     array   $data          Les données associées au contexte du déclenchement (optionelles)
     *
     * @return  bool               'true'  si les traitements se passent sans interruption,
     *                             'false' en cas d'interruption par un hook (en erreur)
    */
    public function run_hook_for_modules(string $hook, array $modules, string $method_prefix, array &$data) {
        $this->log(__METHOD__, "BEGIN");

        $has_failed = false;
        $module_called = array();
        $method_name = $method_prefix.'_'.$hook;
        $error_method_name = $method_prefix.'_'.$hook.'_error';

        foreach($modules as $module) {
            if(method_exists($module, $method_name)) {
                $this->log(__METHOD__, "($hook, $method_prefix) calling ".get_class($module)."::$method_name ...");
                if (! $module->$method_name($data)) {
                    $this->log(__METHOD__, "hook '$method_name' return 'false': stop calling other hooks");
                    $has_failed = true;
                    break;
                }
                $module_called[] = $module;
            }
            else {
                $module_main_method = $module->get_main_method();
                if (! empty($module_main_method) && method_exists($module, $module_main_method)) {
                    $this->log(__METHOD__, "($hook, $method_prefix) calling ".get_class($module)."::$module_main_method ...");
                    if (! $module->$module_main_method($method_name, $data)) {
                        $this->log(__METHOD__, "hook '$method_name' return 'false': stop calling other hooks");
                        $has_failed = true;
                        break;
                    }
                    $module_called[] = $module;
                }
            }
        }
        if ($has_failed) {
            foreach($module_called as $module) {
                if(method_exists($module, $error_method_name)) {
                    $this->log(__METHOD__, "($hook, $method_prefix ) calling ".get_class($module)."::$error_method_name ...");
                    $module->$error_method_name($data);
                }
                else {
                    $module_main_method = $module->get_main_method().'_error';
                    if (! empty($module_main_method) && method_exists($module, $module_main_method)) {
                        $this->log(__METHOD__, "($hook, $method_prefix ) calling ".get_class($module)."::$module_main_method ...");
                        $module->$module_main_method($method_name, $data);
                    }
                }
            }
            $this->log(__METHOD__, "END : FALSE");
            return false;
        }
        $this->log(__METHOD__, "END : TRUE");
        return true;
    }

    /**
     * Renvoi un préfixe pour les 'hooks' qui correspond à la classe de l'objet
     *
     * @param  om_dbform  $object  Objet métier concerné
     *
     * @return string
     */
    protected function get_hook_prefix(om_dbform $object) {
        //return $this->table => FAIL pour le dossier_instruction, dont la table est 'dossier'
        return get_class($object);
    }

    /**
     * Exécute la méthode correspondant au trigger spécifié, pour chaque module de cet objet
     *
     * @param     string  $hook    Le nom du hook
     * @param  om_dbform  $object  Objet métier concerné
     * @param      array  $data    Les données associées au contexte du déclenchement (optionelles)
     *
     * @return  bool               'true'  si les traitements se passent sans interruption,
     *                             'false' en cas d'interruption par un hook (en erreur)
     */
    public function run_hooks(string $hook, om_dbform $object, array &$data = array()) {
        // si les modules sont désactivés on ne déclenche rien
        if (! $this->enabled_for(get_class($object))) {
            //$this->log(__METHOD__, "($hook, ".get_class($object).") '".get_class($object)."': modules not enabled");
            return true;
        }
        $this->log(__METHOD__, "($hook, ".get_class($object).") BEGIN");
        $modules = $this->get_modules_for_object($object, true, $data);

        $result = $this->run_hook_for_modules($hook, $modules, $this->get_hook_prefix($object), $data);

        $this->log(__METHOD__, "($hook, ".get_class($object).") END");
        return $result;
    }

    /**
     * Exécute la méthode correspondant au trigger spécifié, pour chaque module de ce type d'objet
     *
     * @param  string  $hook         Le nom du hook
     * @param  string  $object_name  Type d'objet métier concerné
     * @param   array  $data         Les données associées au contexte du déclenchement (optionelles)
     *
     * @return bool                  'true'  si les traitements se passent sans interruption,
     *                               'false' en cas d'interruption par un hook (en erreur)
     */
    public function run_hooks_for_object_name(string $hook, string $object_name, array &$data = array()) {
        // si les modules sont désactivés on ne déclenche rien
        if (! $this->enabled_for($object_name)) {
            //$this->log(__METHOD__, "($hook, $object_name) '$object_name': modules not enabled");
            return true;
        }
        $this->log(__METHOD__, "($hook, $object_name) BEGIN");

        // instancie un objet vide, permettant d'appeler les méthodes de surcharge des
        // modules (de la classe de l'objet)
        $object_empty = $this->framework->get_inst__om_dbform(array(
            'obj' => $object_name,
            'idx' => ']',
        ));

        // charge les modules en passant par l'objet ou bien la méthode « standard »
        if (is_null($modules = $object_empty->get_modules_override(true))) {
            $modules = $this->get_modules_for_object_name_and_ids($object_name);

            // charge les éventuels modules supplémentaires
            if (! empty($extra_modules = $object_empty->get_modules_append(true))) {
                $modules = array_merge($modules, $extra_modules);
            }
        }

        $result = $this->run_hook_for_modules($hook, $modules, $object_name, $data);

        $this->log(__METHOD__, "($hook, $object_name) END");
        return $result;
    }

    /**
     * Renvoi un identifiant unique pour un objet métier donné (instance d'objet).
     *
     * @param  om_dbform  $object  Objet métier concerné
     *
     * @return string
     */
    public function get_object_hash_from_object(om_dbform $object) {
        $object_name = get_class($object);
        $object_id = $object->getVal($object->clePrimaire);
        return $this->get_object_hash_from_object_name_and_id(
            $object_name, $object_id);
    }

    /**
     * Renvoi un identifiant unique pour un objet métier donné (nom et id d'objet).
     *
     * @param  string  $object_name  Nom d'objet métier concerné
     * @param  string  $object_id    Identifier d'objet métier concerné
     *
     * @return string
     */
    public function get_object_hash_from_object_name_and_id(
            string $object_name, string $object_id) {
        // no id => object class
        if (empty($object_id) && $object_id !== '0') {
            return '['.$object_name.']';
        }
        // new object
        elseif ($object_id === ']') {
            return $object_name.'!new';
        }
        // id not empty and not new
        return $object_name.'#'.$object_id;
    }

    /**
     * Renvoie la liste des instances des modules pour l'object spécifié.
     *
     * Si la liste des instances des modules de cet objet a déjà été produite, alors on la
     * renvoie telle qu'elle a été produite précédemment.
     *
     * @param  om_dbform  $object   L'objet (métier) dont on souhaite les modules
     * @param  bool       $related  Si 'true' alors pour chaque champ de l'objet (différent
     *                              de la clé primaire) alors on charge les modules
     *                              correspondant.
     *
     * @return array  Une liste des instances des modules associés à l'objet spécifié
     */
    public function get_modules_for_object(om_dbform $object, bool $related = true, array $data = array()) {
        $object_name = get_class($object);
        $object_id = $object->getVal($object->clePrimaire);

        // si les modules sont désactivés on ne renvoie rien
        if (! $this->enabled_for($object_name)) {
            //$this->log(__METHOD__, "($object_name, $object_id) '$object_name': modules not enabled");
            return array();
        }

        // une métode de surcharge existe au niveau de l'objet et retourne un 'array'
        if (! is_null($modules = $object->get_modules_override())) {
            // on renvoie les modules définis par l'objet
            return $modules;
        }

        // sinon on commence par récupérer les modules de l'objet
        $object_hash = $this->get_object_hash_from_object($object);
        $this->log(__METHOD__, "($object_hash) BEGIN");
        $modules = $this->get_modules_for_object_name_and_ids($object_name, $object_id, $object);
        $ret = $modules;

        // si l'option "related" est spécifiée
        if ($related) {

            // pour chaque champ de l'objet (qui n'est pas la clé primaire)
            foreach($object->champs as $champ) {
                if ($champ == $object->clePrimaire) {
                    continue;
                }
                // on récupère la valeur du champ
                $related_object_id = $object->getVal($champ);

                // et si celle-ci n'est pas vide
                if (! empty($related_object_id)) {

                    // on récupère les modules correspondant à ce champ
                    $related_object_name = $champ;
                    $related_object_hash = $this->get_object_hash_from_object_name_and_id(
                        $related_object_name, $related_object_id);
                    $modules_related = $this->get_modules_for_object_name_and_ids(
                        $related_object_name, $related_object_id, $object, false);

                    // si on a récupéré des modules on les ajoutent à la liste
                    if (! empty($modules_related)) {
                        $ret = array_merge($ret, $modules_related);
                    }
                }
            }
        }

        // une métode d'addition existe au niveau de l'objet, et son retour n'est pas vide
        if (! empty($extra_modules = $object->get_modules_append(false, $data))) {
            // on ajoute les modules renvoyés par l'objet
            $ret = array_merge($ret, $extra_modules);
        }

        $this->log(__METHOD__, "($object_hash) return:");
        foreach($ret as $m) {
            $this->log(__METHOD__, "($object_hash)   - module: ".$m->to_string());
        }
        $this->log(__METHOD__, "($object_hash) END");
        return $ret;
    }

    /**
     * Renvoie la liste des instances des modules pour l'object spécifié.
     *
     * Si la liste des instances des modules de cet objet a déjà été produite, alors on la
     * renvoie telle qu'elle a été produite précédemment.
     *
     * @param  string        $object_name   Le nom de l'objet dont on veut les modules
     * @param  string|array  $object_ids    Identifiant(s) de l'instance d'objet dont on veut les modules (optionel)
     * @param  om_dbform     $object        L'objet (métier) dont on souhaite les modules (sera rattchés à ceux-ci)
     * @param  bool          $no_class_mod  Si 'true' charge les modules de la classe de l'objet
     *
     * @return array  Une liste des instances des modules associés à l'objet spécifié
     */
    public function get_modules_for_object_name_and_ids(
            string $object_name, $object_ids = null, $object = null,
            bool $class_mod = true) {

        // si les modules sont désactivés on ne renvoie rien
        if (! $this->enabled_for($object_name)) {
            //$this->log(__METHOD__, "($object_name, $object_id) '$object_name': modules not enabled");
            return array();
        }

        if (! is_array($object_ids)) {
            $object_ids = array($object_ids);
        }

        // retire les éléments vides ou nouveau
        $object_ids = array_filter($object_ids,
            function ($id) {
                return (! empty($id) || $id === '0') && $id !== ']';
            });

        $object_ids_text = implode('|', $object_ids);

        $this->log(__METHOD__, "($object_name, $object_ids_text) BEGIN");
        $modules = array();

        // on charge les modules de la classe de l'objet
        if ($class_mod) {

            // récupère les modules pour la classe de l'objet
            $object_class_hash = $this->get_object_hash_from_object_name_and_id($object_name, '');
            if (! isset($this->modules_by_object[$object_class_hash])) {
                $this->log(__METHOD__, "($object_name, $object_ids_text) loading modules for hash".
                                       " '$object_class_hash'");
                $object_class_modules = $this->load_modules_for_object_name_and_ids(
                    $object_name, null, $object);
                $this->modules_by_object[$object_class_hash] = $object_class_modules;
            } else {
                $this->log(__METHOD__, "($object_name, $object_ids_text) getting preloaded modules".
                                       " for hash '$object_class_hash'");
            }

            $modules = $this->modules_by_object[$object_class_hash];
            $this->log(__METHOD__, "($object_name, $object_ids_text) got '".count($modules)."' ".
                                   "modules for hash '$object_class_hash'");
            foreach($modules as $module) {
                $this->log(__METHOD__, "($object_name, $object_ids_text) - ".$module->to_string());
            }
        }

        // si un ou plusieurs identifiants d'objet ont été spécifiés
        if (! empty($object_ids)) {

            // liste des modules des objets (à ajouter ensuite à la liste de retour)
            $added_mods = array();

            // liste des identifiants d'objet pour lesquels il faudra récupérer les modules
            $need_to_load_modules_for = array();

            foreach($object_ids as $object_id) {

                // si l'objet n'a pas déjà eut ses modules chargés
                $object_hash = $this->get_object_hash_from_object_name_and_id(
                    $object_name, $object_id);
                if (! isset($this->modules_by_object[$object_hash])) {

                    // on l'ajoute à la liste des objets dont il faut charger les modules
                    $need_to_load_modules_for[$object_hash] = $object_id;
                    $this->log(__METHOD__, "($object_name, $object_ids_text) no preloaded".
                                            " modules for hash '$object_hash'");

                // sinon on récupère ses modules déjà chargés (s'ils ne sont pas vides)
                } elseif (! empty($this->modules_by_object[$object_hash])) {
                    $this->log(__METHOD__, "($object_name, $object_ids_text) getting preloaded".
                                            " modules for hash '$object_hash'");
                    $object_modules = $this->modules_by_object[$object_hash];
                    $this->log(__METHOD__, "($object_name, $object_ids_text) got '".
                                            count($object_modules)."' modules for hash".
                                            " '$object_hash'");
                    $added_mods = array_merge($added_mods, $object_modules);

                // sinon on log l'info que cet objet n'a pas de module
                } else {
                    $this->log(__METHOD__, "($object_name, $object_ids_text) zero module ".
                                            "for hash '$object_hash'");
                }
            }

            $this->log(__METHOD__, "($object_name, $object_ids_text) preloaded modules are:");
            foreach($added_mods as $module) {
                $this->log(__METHOD__, "($object_name, $object_ids_text) - ".$module->to_string());
            }

            // si des objets n'ont pas eut leurs modules chargés
            if (! empty($need_to_load_modules_for)) {

                // récupère les modules des objets qui le necéssitent
                $this->log(__METHOD__, "($object_name, $object_ids_text) loadings modules for".
                                       " objects ids: $object_ids_text");
                $more_modules = $this->load_modules_for_object_name_and_ids(
                    $object_name, $need_to_load_modules_for, $object);

                // aucun module trouvé
                if (empty($more_modules)) {

                    // on enregistre que ces objets n'ont pas de modules associés
                    foreach(array_keys($need_to_load_modules_for) as $object_hash) {
                        if (! isset($this->modules_by_object[$object_hash])) {
                            $this->modules_by_object[$object_hash] = array();
                            $this->log(__METHOD__, "($object_name, $object_ids_text) saved zero ".
                                                   "modules for hash '$object_hash'");
                        }
                    }

                // des modules ont été trouvés
                } else {

                    // renverse le mapping hash => object_id
                    $mapping_object_id_hash = array_flip($need_to_load_modules_for);

                    // pour chacun des modules retournés
                    foreach($more_modules as $mod) {

                        // récupère l'identifiant de l'objet
                        if (! empty($mod->object_link_instance)) {
                            $object_id = $mod->object_link_instance->getVal('object_id');

                            // récupère le hash
                            $object_hash = $mapping_object_id_hash[$object_id] ?? null;
                            if (! empty($object_hash)) {

                                // ajoute le module à la liste de ceux déjà chargés pour cet objet
                                if (! isset($this->modules_by_object[$object_hash])) {
                                    $this->modules_by_object[$object_hash] = array();
                                }
                                $this->modules_by_object[$object_hash][] = $mod;
                                $this->log(__METHOD__, "($object_name, $object_ids_text) adding module".
                                    " '".$mod->to_string()."' for hash '$object_hash'");
                                $added_mods[] = $mod;
                            } else {
                                $err_msg = "No object hash found for object id '$object_id'";
                                $this->log(__METHOD__, $err_msg, 'ERROR');
                                throw new RuntimeException($err_msg);
                            }
                        } else {
                            $err_msg = "No object link instance for module '".$mod->to_string()."'";
                            $this->log(__METHOD__, $err_msg, 'ERROR');
                            throw new RuntimeException($err_msg);
                        }
                    }
                }
            }

            // ajoute les modules des objets à la liste finale
            if (! empty($added_mods)) {

                $this->log(__METHOD__, "($object_name, $object_ids_text) new loaded modules are:");
                foreach($added_mods as $module) {
                    $this->log(__METHOD__, "($object_name, $object_ids_text) - ".$module->to_string());
                }

                $modules = array_merge($modules, $added_mods);
            }
        }

        // TODO à déplacer après chaque récupération d'un module pré-chargé, pour l'enregistrer
        //      à nouveau avec son module ?
        // assigne l'objet métier courant aux modules qui n'ont aucun objet métier associé
        if (! empty($object)) {
            foreach($modules as $module) {
                if (empty($module->object)) {
                    $module->object = $object;
                    $this->log(__METHOD__, "($object_name, $object_ids_text) - ".
                                           "assigne l'objet métier au module: ".$module->to_string());
                }
            }
        }

        $this->log(__METHOD__, "($object_name, $object_ids_text) END");
        return $modules;
    }

    /**
     * Récupère la liste des modules pour cet objet
     *
     * @param  string   $object_name  Le nom de l'objet dont on veut les modules
     * @param  mixed    $object_ids   Liste ou identifiant de l'instance d'objet dont on veut les modules (optionel)
     * @param  om_dbform  $object     L'objet (métier) dont on souhaite les modules (sera rattchés à ceux-ci)
     * @param  bool     $no_object_id_means_all  si 'true' alors récupère les modules de l'objet associé à au moins une instance.
     *
     * @return array
     */
    protected function load_modules_for_object_name_and_ids(
            string $object_name, $object_ids = null, $object = null, bool $no_object_id_means_all = false) {
        $debug_object_ids = is_array($object_ids) ? implode('|', $object_ids) : var_export($object_ids, true);
        $debug_base = "($object_name, $debug_object_ids, ".var_export($no_object_id_means_all, true).")";
        $this->log(__METHOD__, "$debug_base BEGIN");
        $modules = array();
        $object_name_escaped = $this->framework->escape_string_for_db_query($object_name);
        $sqlWhere = "object_name = '$object_name_escaped'";
        if (! empty($object_ids)) {
            $sqlWhere .= " AND object_id IN (";
            if (! is_array($object_ids)) {
                $object_ids = array($object_ids);
            }
            foreach($object_ids as $index => $object_id) {
                $object_id_escaped = $this->framework->escape_string_for_db_query($object_id);
                $sqlWhere .= " '$object_id_escaped',";
            }
            $sqlWhere = preg_replace('/,$/', '', $sqlWhere);
            $sqlWhere .= " )";
        }
        elseif(! $no_object_id_means_all) {
            $sqlWhere .= ' AND object_id IS NULL';
        }
        elseif($no_object_id_means_all) {
            $sqlWhere .= ' AND object_id IS NOT NULL';
        }
        $this->log(__METHOD__, "$debug_base sql WHERE: $sqlWhere ");
        $wf_module_links = $this->framework->getAllObjects('lien_module', $sqlWhere, 'ordre ASC NULLS LAST');
        if ($wf_module_links === false) return array();
        $this->log(__METHOD__, "$debug_base object_link: ".count($wf_module_links));
        foreach($wf_module_links as $wf_module_link) {
            $module_name = $wf_module_link->getVal('module');
            $module_dir = $this->get_modules_dir();
            $module_script = "$module_dir/$module_name.php";
            $this->log(__METHOD__, "$debug_base script '$module_script'");
            if (! is_file($module_script)) {
                $module_dir = dirname($module_script).'/'.basename($module_script, '.php');
                if (! is_dir($module_dir)) continue;
                $module_script = "$module_dir/$module_name.php";
                if (! is_file($module_script)) continue;
            }

            $this->log(__METHOD__, "$debug_base module: loading module '$module_script'");
            $module_name = basename($module_script, '.php');
            $this->log(__METHOD__, "$debug_base loading module '$module_name' (id:".$wf_module_link->getVal($wf_module_link->clePrimaire).", ordre:".$wf_module_link->getVal('ordre').")");
            require_once $module_script;
            $module_name_camel = str_replace(' ', '', ucfirst(preg_replace('/[^a-zA-Z0-9]/', ' ', $module_name)));
            $module_namespace = "Module\\$module_name_camel";
            $module_class = $module_name;
            $module_class_path = $module_namespace.'\\'.$module_class;
            $this->log(__METHOD__, "$debug_base module: module classpath '$module_class_path'");
            $module_instance = new $module_class_path($module_dir, $this->framework, $wf_module_link, $object);
            $this->log(__METHOD__, "$debug_base module: loaded module '$module_class_path'");
            $module_instance->set_parameters($wf_module_link->getVal('parametres'), 'ini');
            $modules[] = $module_instance;
        }
        $this->log(__METHOD__, "$debug_base END");
        return $modules;
    }

    /**
     * Recherche et instancie les modules pour cet objet
     *
     * @param  om_dbform  $object  Objet métier concerné
     * @param  bool  $with_related  si 'true' fait de même pour les modules liés à cet objet
     *
     * @return array
     */
    /*protected function load_modules(om_dbform &$object, bool $with_related = true) {
        // si les modules sont désactivés on ne les charge pas
        if (! $this->enabled_for(get_class($object))) {
            return array();
        }
        $this->log(__METHOD__, '('.get_class($object).', '.var_export($with_related, true).') BEGIN (modules_loaded: '.var_export($this->modules_loaded, true).')');
        if (! $this->modules_loaded) {
            $this->modules = $this->get_modules();
            $this->log(__METHOD__, "(".var_export($with_related, true).") modules: ".count($this->modules));
            $this->modules_loaded = true;
        }
        $modules = $this->modules;
        if ($with_related) {
            if (! $this->modules_related_all_loaded) {
                $this->log(__METHOD__, '('.var_export($with_related, true).') loading related modules ...');
                foreach($this->champs as $champ) {
                    if ($champ == $this->clePrimaire) {
                        continue;
                    }
                    $modules_related = $this->load_modules_related($champ);
                    if (! empty($modules_related)) {
                        $modules = array_merge($modules, $modules_related);
                    }
                }
                $this->modules_related_all_loaded = true;
            }
            else {
                foreach($this->champs as $champ) {
                    if ($champ == $this->clePrimaire) {
                        continue;
                    }
                    $modules_related = $this->modules_related[$champ] ?? array();
                    if (! empty($modules_related)) {
                        $modules = array_merge($modules, $modules_related);
                    }
                }
            }
        }
        return $modules;
    }*/


    /**
     * Retourne la liste des modules reliés entre l'objet courant et l'object de type spécifié
     *
     * @param  string  $object_name  Le type d'objet lié (typiquement correspond à un nom de classe fille d'om_dbform)
     *
     * @return array
     */
    /*protected function get_modules_related(string $object_name) {
        return $this->get_modules_for($object_name, null, true);
    }*/

    /**
     * Recherche et instancie les modules pour les objets liés
     *
     * @param  om_dbform  $object  Objet métier concerné
     *
     * @return array
     */
    /*protected function load_modules_related(om_dbform $object) {
        $object_name = get_class($object);
        $this->log(__METHOD__, "($object_name) BEGIN (modules_related_loaded: ".var_export(($this->modules_related_loaded[$object_name] ?? false), true).')');
        if (! isset($this->modules_related_loaded[$object_name])
                || ! $this->modules_related_loaded[$object_name]) {
            $this->modules_related[$object_name] = $this->get_modules_related($object_name);
            if (! empty($this->modules_related[$object_name])) {
                $this->log(__METHOD__, "($object_name) modules: ".count($this->modules_related[$object_name]));
            }
            $this->modules_related_loaded[$object_name] = true;
        }
        return $this->modules_related[$object_name];
    }*/

    /**
     * Renvoi un module correspondant à l'instance de lien_module fournie.
     *
     * @param  lien_module  $wf_module_link  Instance de lien module
     * @param       mixed   $object          Un objet métier à associer au module
     *
     * @return module|null
     */
    public function get_module_for_link_instance(lien_module $wf_module_link, $object = null) {
        $module_name = $wf_module_link->getVal('module');
        $module_dir = $this->get_modules_dir();
        $module_script = "$module_dir/$module_name.php";
        $this->log(__METHOD__, "script '$module_script'");

        if (! is_file($module_script)) {
            $module_dir = dirname($module_script).'/'.basename($module_script, '.php');
            if (! is_dir($module_dir)) return null;
            $module_script = "$module_dir/$module_name.php";
            if (! is_file($module_script)) return null;
        }

        $this->log(__METHOD__, "module: loading module '$module_script'");
        $module_name = basename($module_script, '.php');
        $this->log(__METHOD__, "loading module '$module_name' (id:".$wf_module_link->getVal($wf_module_link->clePrimaire).", ordre:".$wf_module_link->getVal('ordre').")");
        require_once $module_script;
        $module_name_camel = str_replace(' ', '', ucfirst(preg_replace('/[^a-zA-Z0-9]/', ' ', $module_name)));
        $module_namespace = "Module\\$module_name_camel";
        $module_class = $module_name;
        $module_class_path = $module_namespace.'\\'.$module_class;
        $this->log(__METHOD__, "module: module classpath '$module_class_path'");
        $module_instance = new $module_class_path($module_dir, $this->framework, $wf_module_link, $object);
        $this->log(__METHOD__, "module: loaded module '$module_class_path'");
        $module_instance->set_parameters($wf_module_link->getVal('parametres'), 'ini');
        return $module_instance;
    }
}
