<?php

namespace apexl\Io\modules\system\services;

use apexl\ClassEngine\EngineSingleton;
use apexl\Config\Singleton;
use apexl\Io\includes\Hook;
use apexl\Io\includes\Routes;
use apexl\Io\includes\System;
use apexl\Io\interfaces\IoModuleInterface;
use apexl\Vault\Vault;
use HaydenPierce\ClassFinder\ClassFinder;

class modules extends EngineSingleton
{
    /** @var array  */
    protected $moduleNamespaces = [];
    /** @var Singleton */
    protected $config;

    protected $loadedModules = [];
    protected $instanciatedModules = [];

    protected $namespaceModules = [];

    protected $services = [];
    /** @var Vault  */
    protected $database;

    protected $loadedFromNamespace = false;

    protected function __construct()
    {
        parent::__construct();
        $this->config = Singleton::getInstance();

        //register any additional module namespaces to load.
        $this->moduleNamespaces = $config->app->module->namespaces ?? [];

        // Allow override at project top-level - in module folder
        if (file_exists($_SERVER['DOCUMENT_ROOT'].'/../module')) {
            $this->moduleNamespaces[] = 'app\module';
        }

        //add the core namespace last.
        $this->moduleNamespaces[] = 'apexl\Io\modules';

        $this->database = Vault::getInstance();
    }

    /**
     * Function to get modules in all known namespaces.
     * @todo add caching class to allow the application to cache into a service such as memcache or redis.
     * @throws \Exception
     */
    public function getAvailableModules()
    {
        $system = System::getInstance();
        //test if the modules table exists, only if we're installed. If it does, load our modules from there.
        if ($system->isInstalled()) {
            try {
                $modules = $this->getActiveModules();
                foreach ($modules as $active) {
                    $this->loadedModules[$active->name] = $active->namespace;
                }
            } catch (\Exception $e) {
                error_log("[IO Exception :: Available Modules] -". $e->getCode()." | ".$e->getMessage());
                $this->loadedModules = $this->getModulesFromNamespace();
            }
        } else {
            $this->loadedModules = $this->getModulesFromNamespace();
        }
    }
    public function getModulesFromNamespace()
    {
        if ($this->loadedFromNamespace) {
            //make sure we return the previously loaded modules.
            return $this->namespaceModules;
        }
        $loadedModules = [];
        foreach ($this->moduleNamespaces as $namespace) {
            $classes = ClassFinder::getClassesInNamespace($namespace, ClassFinder::RECURSIVE_MODE);
            foreach ($classes as $class) {
                //we only want to name immediately after the modules namespace (for now) so strip the rest.
                $moduleSpace = str_replace($namespace . '\\', '', (string) $class);
                $moduleSpaces = explode('\\', $moduleSpace);
                $moduleName = array_shift($moduleSpaces);
                if (!isset($loadedModules[$moduleName])) {
                    $module = '\\' . $namespace . '\\' . $moduleName . '\\' . $moduleName . 'Module';
                    $loadedModules[$moduleName] = $module;
                }
            }
        }
        $this->loadedFromNamespace = true; //prevent duplicated namespace loading which leads to cannot redeclare errors.
        $this->namespaceModules = $loadedModules;
        return $loadedModules;
    }

    public function addModule($namespace)
    {
        $namespace = explode('\\', (string) $namespace);
        $this->loadedModules[end($namespace)] = $namespace;
    }

    /**
     * Method to initialise available modules.
     * @todo change this to only initialise installed or those that dont need install.
     * @return array
     */
    public function initialiseActiveModuleServices()
    {
        //first loop over the modules and add any services they register.
        foreach ($this->loadedModules as $module) {
            //we have to double instantiate so that $app is available the second time around.
            if (class_exists($module)) {
                $module = new $module();
                if ($module->isInstalled()) {
                    $this->initialiseModuleServices($module);
                    //allow modules to register service callbacks before we process things.
                    if (method_exists($module, 'registerCallbacks')) {
                        $module->registerCallbacks();
                    }
                    $this->instanciatedModules[] = $module;
                }
            } else {
                error_log("#########");
                error_log('Module ' . $module . ' cannot be found.');
                error_log("#########");
            }
        }

        return $this->services;
    }

    /**
     * Get the loaded modules.
     * @return array
     */
    public function getLoadedModules()
    {
        return $this->loadedModules;
    }

    protected function initialiseModuleServices(IoModuleInterface $module)
    {
        if (method_exists($module, 'addServices')) {
            $this->services = array_merge($this->services, $module->addServices());
        }
    }

    public function initialiseActiveModules()
    {
        //use the already instanciated modules and initialise them
        foreach ($this->instanciatedModules as $module) {
            //we have to double instantiate so that $app is available the second time around.
            if ($module->isInstalled()) {
                $this->initialiseModule($module);
            }
        }
    }

    /**
     * Push the provided module to the top of the loaded list.
     * @param $module
     */
    public function setModuleFirst($module)
    {
        if (isset($this->loadedModules[$module])) {
            $path = $this->loadedModules[$module];
            unset($this->loadedModules[$module]);
            $this->loadedModules = [$module => $path] + $this->loadedModules;
        }
    }

    public function getModule($name, $namespaceOnly = false)
    {
        $loadedModules = $this->getModulesFromNamespace();
        if (isset($loadedModules[$name])) {
            $namespace = $loadedModules[$name];
            if ($namespaceOnly) {
                return $namespace;
            }
            return new $namespace();
        } else {
            throw new \Exception("The requested module (".$name.") doesnt exist / isn't composer installed.", 400);
        }
    }

    public function getActiveModules()
    {
        return $this->database->select('modules')->fields()->where('installed', 1)->orderBy('weight', 'ASC')->execute()->fetchAll();
    }

    /**
     * Initialise modules. Calls a few core methods and allows other modules to extend the methods used.
     */
    protected function initialiseModule(IoModuleInterface $module)
    {
        //initialise module now that app is available
        $module->initialise($this->config, $this->database);

        //Then we need to register the routes, we do this with reflection
        if (method_exists($module, 'routes')) {
            $module->routes();
        }

        //not all modules will add middleware so add a check for those that do.
        if (method_exists($module, 'addMiddleware')) {
            $module->addMiddleware();
        }

        if (method_exists($module, 'addRouteArgs')) {
            $module->addRouteArgs();
        }

        //allow other services to intercept this process to add additional functionality.
        //Allows for 'safe' required method calls to be added, for example user permissions, menu building etc
        Hook::processHook("initialiseModule", $module);
    }
}
