<?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\Module;

use Db;
use Exception;
use Proxim\Application;
use Proxim\Cache\Cache;
use Proxim\Configuration;
use Proxim\Hook;
use Proxim\Order\Meta;
use Proxim\Tools;
use Proxim\Validate;

abstract class Module implements ModuleInterface
{
    /** @var int Module ID */
    public $id = null;

    /** @var float Version */
    public $version;

    public $database_version;

    /** @var string Unique name */
    public $name;

    /** @var string Human name */
    public $displayName;

    /** @var string A little description of the module */
    public $description;

    /** @var string author of the module */
    public $author;

    /** @var string URI author of the module */
    public $author_uri = '';

    /** @var string icon */
    public $icon;

    /** @var bool Status */
    public $active = false;

    /** @var bool Free */
    public $free = false;

    /** @var float Amount */
    public $amount = 0;

    /**
     * @since 1.5.0.1
     *
     * @var string Registered Version in database
     */
    public $registered_version;

    /** @var array Array cache filled with modules informations */
    public static $modules_cache;

    /** @var array names of the controllers */
    public $controllers = array();

    /** @var string Module web path (eg. '/proxim/content/modules/modulename/') */
    protected $_path = null;

    /**
     * @since 1.5.0.1
     *
     * @var string Module local path (eg. '/home/proxim/content/modules/modulename/')
     */
    protected $local_path = null;

    /** @var string Main table used for modules installed */
    protected $table = 'module';

    /** @var string Identifier of the main table */
    protected $identifier = 'module_id';

    /** @var array Array cache filled with modules instances */
    public static $_INSTANCE = array();

    public static $purchase_link = "https://craftyworks.co";

    /** @var Application */
    public $application;

    /** @var Smarty_Data */
    public $smarty;

    const CACHE_FILE_MODULES_LIST = 'content/xml/modules_list.xml';
    
    /**
     * Constructor.
     *
     * @param string $name Module unique name
     * @param Application $application
     */
    public function __construct($name = null, Application $application = null)
    {
        if (isset($this->prox_versions_compliancy) && !isset($this->prox_versions_compliancy['min'])) {
            $this->prox_versions_compliancy['min'] = '1.0.0';
        }

        if (isset($this->prox_versions_compliancy) && !isset($this->prox_versions_compliancy['max'])) {
            $this->prox_versions_compliancy['max'] = Application::VERSION;
        }

        if (strlen($this->prox_versions_compliancy['min']) == 3) {
            $this->prox_versions_compliancy['min'] .= '.0.0';
        }

        if (strlen($this->prox_versions_compliancy['min']) == 5) {
            $this->prox_versions_compliancy['min'] .= '.0';
        }

        if (strlen($this->prox_versions_compliancy['max']) == 5) {
            $this->prox_versions_compliancy['max'] .= '.999';
        }

        if (strlen($this->prox_versions_compliancy['max']) == 3) {
            $this->prox_versions_compliancy['max'] .= '.999.999';
        }

        // Load context and smarty
        $this->application = $application ? $application : Application::getInstance();
        if (is_object($this->application->container->smarty)) {
            $this->smarty = $this->application->container->smarty;
        }

        // If the module has no name we gave him its id as name
        if ($this->name === null) {
            $this->name = $this->id;
        }

        // If the module has the name we load the corresponding data from the cache
        if ($this->name != null) {
            // If cache is not generated, we generate it
            if (self::$modules_cache == null && !is_array(self::$modules_cache)) {
                self::$modules_cache = array();
                // Join clause is done to check if the module is activated in current shop context
                $result = Db::getInstance()->executeS('
                    SELECT m.`module_id`, m.`name`, m.`active`
                    FROM ' . Db::prefix('module') . ' m
                ');
                
                foreach ($result as $row) {
                    self::$modules_cache[$row['name']] = $row;
                    self::$modules_cache[$row['name']]['active'] = (bool) $row['active'];
                }
            }

            // We load configuration from the cache
            if (isset(self::$modules_cache[$this->name])) {
                if (isset(self::$modules_cache[$this->name]['module_id'])) {
                    $this->id = self::$modules_cache[$this->name]['module_id'];
                }

                foreach (self::$modules_cache[$this->name] as $key => $value) {
                    if (array_key_exists($key, $this)) {
                        $this->{$key} = $value;
                    }
                }

                $this->_path = PROX_DIR_ROOT . 'modules/' . $this->name . '/';
            }

            $this->local_path = PROX_DIR_MODULE . $this->name . '/';
        }
    }

    /**
     * Return an instance of the specified module.
     *
     * @param string $module_name Module name
     *
     * @return Module
     */
    public static function getInstanceByName($module_name)
    {
        if (!Validate::isModuleName($module_name)) {
            return false;
        }

        if (!isset(self::$_INSTANCE[$module_name])) {
            if (!Tools::file_exists_no_cache(PROX_DIR_MODULE . $module_name . '/' . $module_name . '.php')) {
                return false;
            }

            return Module::coreLoadModule($module_name);
        }

        return self::$_INSTANCE[$module_name];
    }

    /**
     * Get ID module by name.
     *
     * @param string $name
     *
     * @return int Module ID
     */
    public static function getModuleIdByName($name)
    {
        $cache_id = 'Module::getModuleIdByName_' . pSQL($name);
        if (!Cache::isStored($cache_id)) {
            $result = (int) Db::getInstance()->getValue('SELECT `module_id` FROM ' . Db::prefix('module') . ' WHERE `name` = "' . pSQL($name) . '"');
            Cache::store($cache_id, $result);

            return $result;
        }

        return Cache::retrieve($cache_id);
    }

    protected static function coreLoadModule($module_name)
    {
        include_once PROX_DIR_MODULE . $module_name . '/' . $module_name . '.php';

        $r = false;

        if (class_exists($module_name, false)) {
            $r = self::$_INSTANCE[$module_name] = Application::getInstance()->{$module_name} = new $module_name();
        }

        return $r;
    }

    /**
     * Check if the module is available for the current user.
     */
    public function checkAccess()
    {
        return true;
    }

    /**
     * @param string $module_name
     *
     * @return bool
     *
     * @deprecated since 1.7
     */
    public static function isInstalled($module_name)
    {
        if (!Cache::isStored('Module::isInstalled' . $module_name)) {
            $module_id = Module::getModuleIdByName($module_name);
            Cache::store('Module::isInstalled' . $module_name, (bool) $module_id);

            return (bool) $module_id;
        }

        return Cache::retrieve('Module::isInstalled' . $module_name);
    }

    public static function isEnabled($module_name)
    {
        if (!Cache::isStored('Module::isEnabled' . $module_name)) {
            $active = false;
            $module_id = Module::getModuleIdByName($module_name);
            if (Db::getInstance()->getValue('SELECT `module_id` FROM ' . Db::prefix('module') . ' WHERE `module_id` = ' . (int) $module_id . ' AND `active` = 1')) {
                $active = true;
            }
            Cache::store('Module::isEnabled' . $module_name, (bool) $active);

            return (bool) $active;
        }

        return Cache::retrieve('Module::isEnabled' . $module_name);
    }

    /**
     * Connect module to a hook.
     *
     * @param string $hook_name Hook name
     *
     * @return bool result
     */
    public function registerHook($hook_name)
    {
        return Hook::registerHook($this, $hook_name);
    }

    /**
     * Unregister module from hook.
     *
     * @param mixed $id_hook Hook id (can be a hook name since 1.5.0)
     *
     * @return bool result
     */
    public function unregisterHook($hook_id)
    {
        return Hook::unregisterHook($this, $hook_id);
    }

    /**
     * Insert module into datable.
     */
    public function install()
    {
        Hook::exec('actionModuleInstallBefore', array('object' => $this));
        // Check module name validation
        if (!Validate::isModuleName($this->name)) {
            throw new Exception('Unable to install the module (Module name is not valid).');
        }

        // Check if module is installed
        $result = $this->isInstalled($this->name);
        if ($result) {
            throw new Exception('This module has already been installed.');
        }

        // Install module and retrieve the installation id
        $result = Db::getInstance()->insert(
            $this->table, 
            array('name' => $this->name, 'active' => 1, 'version' => $this->version)
        );

        if (!$result) {
            throw new Exception('Technical error: Proxim could not install this module.');
        }

        $this->id = Db::getInstance()->Insert_ID();

        Cache::clean('Module::isInstalled' . $this->name);

        Hook::exec('actionModuleInstallAfter', array('object' => $this));

        return true;
    }

    /**
     * Delete module from datable.
     *
     * @return bool result
     */
    public function uninstall()
    {
        // Check module installation id validation
        if (!Validate::isUnsignedId($this->id)) {
            throw new Exception('The module is not installed.');
        }

        // Retrieve hooks used by the module
        $sql = 'SELECT DISTINCT(`hook_id`) FROM ' . Db::prefix('hook_module') . ' WHERE `module_id` = ' . (int) $this->id;
        $result = Db::getInstance()->executeS($sql);
        foreach ($result as $row) {
            $this->unregisterHook((int) $row['hook_id']);
        }

        // Uninstall the module
        if (Db::getInstance()->execute('DELETE FROM ' . Db::prefix('module') . ' WHERE `module_id` = ' . (int) $this->id)) {
            Cache::clean('Module::isInstalled' . $this->name);
            Cache::clean('Module::getModuleIdByName_' . pSQL($this->name));
            return true;
        }

        return false;
    }

    public static function configXmlStringFormat($string)
    {
        return Tools::htmlentitiesDecodeUTF8($string);
    }

    /**
     * Use this method to return the result of a smarty template when assign data only locally with $this->smarty->assign().
     *
     * @param string $templatePath relative path the template file, from the module root dir
     *
     * @return mixed
     */
    public function fetch($templatePath)
    {
        $template = $this->application->smarty->createTemplate(
            $templatePath,
            $this->smarty
        );

        return $template->fetch();
    }

    protected static function useTooMuchMemory()
    {
        $memory_limit = Tools::getMemoryLimit();
        if (function_exists('memory_get_usage') && $memory_limit != '-1') {
            $current_memory = memory_get_usage(true);
            $memory_threshold = (int) max($memory_limit * 0.15, Tools::isX86_64arch() ? 4194304 : 2097152);
            $memory_left = $memory_limit - $current_memory;

            if ($memory_left <= $memory_threshold) {
                return true;
            }
        }

        return false;
    }

    /**
     * Return modules directory list.
     *
     * @return array Modules Directory List
     */
    public static function getModulesDirOnDisk()
    {
        $module_list = array();
        $modules = scandir(PROX_DIR_MODULE, SCANDIR_SORT_NONE);
        foreach ($modules as $name) {
            if (is_file(PROX_DIR_MODULE . $name)) {
                continue;
            } elseif (is_dir(PROX_DIR_MODULE . $name . DIRECTORY_SEPARATOR) && Tools::file_exists_cache(PROX_DIR_MODULE . $name . '/' . $name . '.php')) {
                if (!Validate::isModuleName($name)) {
                    throw new Exception(sprintf('Module %s is not a valid module name', $name));
                }
                $module_list[] = $name;
            }
        }

        return $module_list;
    }

    /**
     * Return available modules.
     *
     * @return array Modules
     */
    public static function getModulesOnDisk()
    {
        $app = Application::getInstance();;

        // Init var
        $module_list = array();
        $module_name_list = array();
        $modules_name_to_cursor = array();

        // Get modules directory list and memory limit
        $modules_dir = Module::getModulesDirOnDisk();

        $modules_installed = array();
        $result = Db::getInstance()->executeS('
            SELECT m.name, m.version
            FROM ' . Db::prefix('module') . ' m'
        );

        foreach ($result as $row) {
            $modules_installed[$row['name']] = $row;
        }

        foreach ($modules_dir as $module) {
            $module_errors = array();
            if (Module::useTooMuchMemory()) {
                $module_errors[] = 'All modules cannot be loaded due to memory limit restrictions, please increase your memory_limit value on your server configuration';
                break;
            }
            
            // Check if config.xml module file exists and if it's not outdated
            $config_file = PROX_DIR_MODULE . $module . '/config.xml';

            $xml_exist = (file_exists($config_file));

            // If config.xml exists 
            if ($xml_exist) {
                // Load config.xml
                libxml_use_internal_errors(true);
                $xml_module = @simplexml_load_file($config_file);
                if (!$xml_module) {
                    $module_errors[] = sprintf(
                        '%s could not be loaded.',
                        array($config_file)
                    );

                    break;
                }

                foreach (libxml_get_errors() as $error) {
                    $module_errors[] = '[' . $module . '] Error found in config file: ' . htmlentities($error->message);
                }

                libxml_clear_errors();

                // If no errors in Xml, no need instand and no need new config.xml file, we load only translations
                if (!count($module_errors) && (int) $xml_module->need_instance == 0) {
                    $item = new \stdClass();
                    $item->id = 0;
                    $item->warning = '';

                    foreach ($xml_module as $k => $v) {
                        $item->$k = (string) $v;
                    }

                    $item->displayName = stripslashes(Module::configXmlStringFormat($xml_module->displayName));
                    $item->description = stripslashes(Module::configXmlStringFormat($xml_module->description));
                    $item->author = stripslashes(Module::configXmlStringFormat($xml_module->author));
                    $item->author_uri = (isset($xml_module->author_uri) && $xml_module->author_uri) ? stripslashes($xml_module->author_uri) : false;
                    $item->not_on_disk = 0;
                    $item->configure = isset($xml_module->configure) ? 1 : 0;

                    if(isset($xml_module->price)) {
                        $item->price = (float) stripslashes($xml_module->price);
                        $item->buylink = self::$purchase_link . "?module=" . $item->name . "&domain=" . $app->request->getHost();
                    }

                    if (@filemtime(PROX_DIR_MODULE . $module . DIRECTORY_SEPARATOR . 'logo.png')) {
                        $item->logo = $app->modules_uri . '/' . $module . '/logo.png';
                    } else {
                        $item->logo = stripslashes(Module::configXmlStringFormat($xml_module->logo));
                    }

                    if (isset($xml_module->confirmUninstall)) {
                        $item->confirmUninstall = html_entity_decode(Module::configXmlStringFormat($xml_module->confirmUninstall));
                    }

                    $item->active = 0;
                    $item->onclick_option = false;
                    $module_list[$item->name . '_disk'] = $item;
                    $module_name_list[] = '\'' . pSQL($item->name) . '\'';
                    $modules_name_to_cursor[Tools::strtolower((string) ($item->name))] = $item;
                }
            }            
        }

        // Get modules information from database
        if (!empty($module_name_list)) {
            $results = Db::getInstance()->executeS('
                SELECT m.module_id, m.name, m.active
                FROM ' . Db::prefix('module') . ' m
                WHERE LOWER(m.name) IN (' . Tools::strtolower(implode(',', $module_name_list)) . ')
            ');

            foreach ($results as $result) {
                if (isset($modules_name_to_cursor[Tools::strtolower($result['name'])])) {
                    $module_cursor = $modules_name_to_cursor[Tools::strtolower($result['name'])];
                    $module_cursor->id = (int) $result['module_id'];
                    $module_cursor->active = (bool) $result['active'];
                }
            }
        }

        $files_list = array(
            array('type' => 'addonsNative', 'file' => PROX_DIR_ROOT . self::CACHE_FILE_MODULES_LIST),
        );

        foreach ($files_list as $f) {
            if (file_exists($f['file'])) {
                $file = $f['file'];
                $content = Tools::file_get_contents($file);
                $xml = @simplexml_load_string($content, null, LIBXML_NOCDATA);

                if ($xml && isset($xml->module)) {
                    foreach ($xml->module as $modaddons) {
                        $flag_found = 0;

                        foreach ($module_list as $k => &$m) {
                            if (Tools::strtolower($m->name) == Tools::strtolower($modaddons->name) && !isset($m->available_on_addons)) {
                                $flag_found = 1;
                                if ($m->version != $modaddons->version && version_compare($m->version, $modaddons->version) === -1) {
                                    $module_list[$k]->version_addons = $modaddons->version;
                                }
                            }
                        }

                        if ($flag_found == 0) {
                            $item = new \stdClass();
                            $item->id = 0;
                            $item->name = strip_tags((string) $modaddons->name);
                            $item->displayName = strip_tags((string) $modaddons->displayName);
                            $item->description = stripslashes(strip_tags((string) $modaddons->description));
                            $item->author = strip_tags((string) $modaddons->author);

                            if (@filemtime(PROX_DIR_MODULE . $modaddons->name . DIRECTORY_SEPARATOR . 'logo.png')) {
                                $item->logo = $app->modules_uri . '/' . $modaddons->name . '/logo.png';
                            } else {
                                $item->logo = strip_tags((string) $modaddons->logo);
                            }

                            $item->need_instance = 0;
                            $item->configure = isset($modaddons->configure) ? 1 : 0;
                            $item->not_on_disk = 1;
                            $item->active = 0;
                            $item->url = isset($modaddons->url) ? $modaddons->url : null;

                            if(isset($modaddons->price)) {
                                $item->price = (float) stripslashes($modaddons->price);
                                $item->buylink = self::$purchase_link . "?module=" . $modaddons->name . "&domain=" . $app->request->getHost();
                            }

                            $module_list[$item->name . '_feed'] = $item;

                            if (isset($module_list[$modaddons->name . '_disk'])) {
                                $item->not_on_disk = 0;
                                $module_list[$modaddons->name . '_disk']->not_on_disk = 0;
                            }
                        }
                    }
                    
                }
            }
        }

        foreach ($module_list as $key => &$module) {
            if (isset($modules_installed[$module->name])) {
                $module->database_version = $modules_installed[$module->name]['version'];
                $module->installed = true;
            } else {
                $module->database_version = 0;
                $module->installed = false;
            }
        }

        usort($module_list, function ($a, $b) {
            return strnatcasecmp($a->displayName, $b->displayName);
        });

        return $module_list;
    }
}