<?php

/**
 * Le namespace auxquels tous les modules doivent appartenir
 */
namespace Module;

use \RuntimeException;
use \InvalidArgumentException;

require_once __DIR__.'/utils.class.php';
use \application;

require_once __DIR__.'/lien_module.class.php';
use \lien_module;

require_once __DIR__.'/om_dbform.class.php';
use \om_dbform;


/**
 * Classe (abstraite) représentant un module
 */
abstract class module {

    /**
     * Une instance du framework openmairie
     *
     * @var application
     */
    protected application $framework;

    /**
     * Le chemin vers le répertoire contenant ce module
     * @var string
     */
    public string $dir;

    /**
     * Une configuration statique
     */
    public array $conf = array();

    /**
     * Une liste de paramètres dynamiques
     */
    public array $params = array();

    /**
     * Une instance du lien avec l'objet auquel est associé ce module
     */
    public ?lien_module $object_link_instance = null;

    /**
     * Une instance de l'objet métier auquel est associé ce module
     */
    public ?om_dbform $object = null;

    /**
     * Renvoie la description du module (type libellé)
     *
     * @return string
     */
    abstract public function get_description();

    /**
     * Installe le module.
     *
     * @return bool  'true' si l'installation s'est bien passée, 'false' sinon
     */
    abstract public function install();

    /**
     * Désinstalle le module.
     *
     * @return bool  'true' si la désinstallation s'est bien passée, 'false' sinon
     */
    abstract public function uninstall();

    /**
     * Créer une instance de module
     *
     * @param  string          $dir  Le répertoire contenant le module
     * @param  application  $framework             Une instance du framework openmairie
     * @param  lien_module     $object_link_instance  Une instance d'objet lien
     * @param  om_dbform       $object  Une instance d'objet métier
     *
     * @return module
     */
    public function __construct(string $dir, application $framework, ?lien_module $object_link_instance = null, ?om_dbform $object = null) {
        $this->framework = $framework;
        $this->log(__METHOD__, '');
        $this->dir = $dir;
        $this->object_link_instance = $object_link_instance;
        $this->object = $object;
        $this->load_conf();
    }

    /**
     * Renvoie une représentation en "string" du module
     *
     * @return string
     */
    public function to_string() {
        $str = sprintf(
            '%s (id:%s, ordre:%s)',
            $this->get_short_name(),
            $this->get_object_link_instance_id(),
            $this->object_link_instance->getVal('ordre'));
        if (empty($this->object_link_instance)) {
            $str = sprintf(
                '%s (no object_link)',
                $this->get_short_name());
        }
        return $str;
    }

    /**
     * Permet d'effectuer des actions lors de l'instanciation
     *
     * @return void
     */
    public function setup() {
        $this->log(__METHOD__, 'BEGIN');
        $this->log(__METHOD__, 'END');
    }

    /**
     * Charge la configuration statique
     *
     * @return void
     */
    public function load_conf() {
        $conf_path = $this->get_conf_path();
        if (is_file($conf_path)) {
            $conf_path_ext = pathinfo($conf_path, PATHINFO_EXTENSION);
            switch($conf_path_ext) {
                case 'php':
                    $this->conf = include $conf_path;
                    break;
                case 'ini':
                    $this->conf = parse_ini_file($conf_path);
                    break;
                case 'json':
                    $this->conf = json_decode(
                        file_get_contents($conf_path), true,
                        512, JSON_THROW_ON_ERROR);
                    break;
            }
        }
    }

    /**
     * Renvoie le chemin vers la configuration statique du module
     *
     * @return string
     */
    protected function get_conf_path() {
        $this->log(__METHOD__, 'BEGIN');
        $module_name = $this->get_short_name();
        $ret = $this->dir."/$module_name.conf.php";
        $this->log(__METHOD__, 'return: '.var_export($ret, true));
        $this->log(__METHOD__, 'END');
        return $ret;
    }

    /**
     * Renvoie le nom court du module
     *
     * @return string
     */
    public function get_short_name() {
        $ref = new \ReflectionClass($this);
        return $ref->getShortName();
    }

    /**
     * Renvoie une URL (ou partie d'URL) pointant vers le répertoire contenant les "assets"
     * (ressources publiques) du module
     *
     * @return string
     */
    public function get_url_assets_path_base() {
        $this->log(__METHOD__, 'BEGIN');
        $base_dir = $this->dir;
        $app_dir = dirname(__DIR__);
        if (strpos($base_dir, $app_dir) !== 0) {
            throw new RuntimeException(
                "Invalid module directory '$base_dir' or application directory '$app_dir'"
            );
        }
        $base_url = str_replace($app_dir, '', $base_dir);
        $url = "../$base_url";
        $url_encoded = implode(
            '/', array_map(
                function ($v) { return rawurlencode($v); },
                explode('/', $url)));
        $ret = $url_encoded;
        $this->log(__METHOD__, 'return: '.var_export($ret, true));
        $this->log(__METHOD__, 'END');
        return $ret;
    }

    /**
     * Récupère la valeur d'une directive de configuration
     *
     * @param  string  $key  Le nom de la directive de configuration dont on veut la valeur
     *
     * @return mixed
     */
    protected function conf(string $key) {
        return $this->conf[$key] ?? null;
    }

    /**
     * Assigne/Enregistre les paramètres du modules.
     * Les formats d'entrée supportés sont INI et JSON.
     *
     * @param  string  $raw_params  Une chaine contenant les paramètres dans un format supporté
     * @param  string  $format      Le nom du format de la chaine des paramètres
     *
     * @return void
     */
    public function set_parameters(string $raw_params, string $format = 'INI') {
        $this->log(__METHOD__, "($format) BEGIN");
        switch(strtoupper($format)) {
            case 'INI':
                $this->log(__METHOD__, "parsing INI string:");
                $this->log(__METHOD__, var_export($raw_params, true));
                $params = parse_ini_string($raw_params);
                if ($params === false) {
                    $this->log(__METHOD__, "Failed to parse INI string: '$raw_params'", 'WARNING');
                }
                if (is_array($params)) {
                    $this->params = $params;
                }
                break;
            case 'JSON':
                $this->log(__METHOD__, "parsing JSON string:");
                $this->log(__METHOD__, var_export($raw_params, true));
                $params = json_decode(
                    $raw_params, true, 512/*, JSON_THROW_ON_ERROR*/);
                if (json_last_error() !== JSON_ERROR_NONE) {
                    $this->log(__METHOD__, var_export(json_last_error_msg(), true), 'WARNING');
                }
                if (is_array($params)) {
                    $this->params = $params;
                }
                break;
        }
        $this->log(__METHOD__, 'END');
    }

    /**
     * Renvoi le nom d'une méthode principale du module.
     * Celle-ci sera appelée à chaque hook et aurait en argument:
     * - le nom du hook
     * - l'objet courant
     * - les données du contaxte
     *
     * @return string|null
     */
    public function get_main_method() {
        $this->log(__METHOD__, 'BEGIN');
        $ret = null;
        $this->log(__METHOD__, 'return: '.var_export($ret, true));
        $this->log(__METHOD__, 'END');
        return $ret;
    }

    /**
     * Renvoie une liste d'action de portlet
     *
     * @return array
     */
    public function get_actions() {
        $this->log(__METHOD__, 'BEGIN');
        $ret = array();
        $this->log(__METHOD__, 'return: '.var_export($ret, true));
        $this->log(__METHOD__, 'END');
        return $ret;
    }

    /**
     * Renvoie la liste des déclencheurs (hooks) supporté par ce module
     *
     * @param  boolean  $in_business_term  'true' pour renvoyer la liste avec des termes business
     *
     * @return array
     */
    public function get_supported_hooks(bool $in_business_term = false) {
        $this->log(__METHOD__, 'BEGIN');
        $ret = array();
        $this->log(__METHOD__, 'return: '.var_export($ret, true));
        $this->log(__METHOD__, 'END');
        return $ret;
    }

    /**
     * Renvoie la liste des scripts Javascript a inclure dans la balise HTML <head>
     *
     * @return array
     */
    public function get_header_scripts_js() {
        $this->log(__METHOD__, 'BEGIN');
        $ret = array();
        $this->log(__METHOD__, 'return: '.var_export($ret, true));
        $this->log(__METHOD__, 'END');
        return $ret;
    }

    /**
     * Renvoie la liste des scripts Javascript a inclure dans le code HTML inline
     *
     * @param  string  $position  'top' pour inclure au début et 'bottom' à la fin
     *
     * @return array
     */
    public function get_inline_scripts_js(string $position) {
        $this->log(__METHOD__, "($position) BEGIN");
        if (! in_array($position, array('top', 'bottom'))) {
            throw InvalidArgumentException("Invalid position '$position' (must be: 'top' or 'bottom')");
        }
        $ret = array();
        $this->log(__METHOD__, 'return: '.var_export($ret, true));
        $this->log(__METHOD__, 'END');
        return $ret;
    }

    /**
     * Renvoie la liste des styles CSS a inclure dans la balise HTML <head>
     *
     * @return array
     */
    public function get_header_styles_css() {
        $this->log(__METHOD__, 'BEGIN');
        $ret = array();
        $this->log(__METHOD__, 'return: '.var_export($ret, true));
        $this->log(__METHOD__, 'END');
        return $ret;
    }

    /**
     * Renvoie la liste des styles CSS a inclure dans le code HTML inline
     *
     * @param  string  $position  'top' pour inclure au début et 'bottom' à la fin
     *
     * @return array
     */
    public function get_inline_styles_css(string $position) {
        $this->log(__METHOD__, "($position) BEGIN");
        if (! in_array($position, array('top', 'bottom'))) {
            throw InvalidArgumentException("Invalid position '$position' (must be: 'top' or 'bottom')");
        }
        $ret = array();
        $this->log(__METHOD__, 'return: '.var_export($ret, true));
        $this->log(__METHOD__, 'END');
        return $ret;
    }

    /**
     * Renvoie l'identifiant de la classe de l'objet lien, ou
     * null s'il n'y a pas d'objet lien
     *
     * @return int|null
     */
    public function get_object_link_instance_id() {
        //$this->log(__METHOD__, 'BEGIN');
        if (! empty($this->object_link_instance)) {
            $id_key = $this->object_link_instance->clePrimaire;
            $ret = $this->object_link_instance->getVal($id_key);
            //$this->log(__METHOD__, 'return: '.var_export($ret, true));
            //$this->log(__METHOD__, 'END');
            return $ret;
        }
        $ret = null;
        //$this->log(__METHOD__, 'return: '.var_export($ret, true));
        //$this->log(__METHOD__, 'END');
        return $ret;
    }

    /**
     * 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') {

        // remplace la classe parente par la classe courante
        $ref = new \ReflectionClass($this);
        $parent_class_escaped = str_replace('\\', '\\\\', $ref->getParentClass()->getName());
        $method = preg_replace(
            '/^'.$parent_class_escaped.'/',
            $ref->getName(),
            $method);

        $namespace_escaped = str_replace('\\', '\\\\', $ref->getNamespaceName());
        $method_short = preg_replace('/^'.$namespace_escaped.'\\\\/', '', $method);

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