<?php
namespace apexl\Io\includes;

use apexl\Config\Singleton;
use apexl\Io\interfaces\IoModuleInterface;
use apexl\Io\recommendedIncludes\apiModule;
use apexl\Io\services\pathUtility;
use apexl\Vault\Vault;
use DI\Bridge\Slim\Bridge;

abstract class Module implements IoModuleInterface{

    /** @var Bridge */
    protected $Io;
    protected $path;
    /** @var Singleton */
    protected $config;
    /** @var Routes */
    protected $route;
    /** @var Vault */
    protected $database;

    protected $baseRoute = '';
    protected $defaultEntityPatterns;
    protected $patternsOverridden = FALSE;

    protected $entityPatterns = [];
    protected $entityControllers = [];

    protected $isInstalled = NULL;

    public function __construct(){
        $this->setBaseRoute(strtolower($this->getName()));
        $this->isInstalled();
    }

    public function initialise(Singleton $config, Vault $database){
        $this->Io = System::$app;
        $this->config = $config;
        $this->database = $database;
        $this->route = Routes::getInstance();
        $this->path = pathUtility::getInstance();
    }

    public function requiresInstall(){
        return !empty($this->schema());
    }

    public function setBaseRoute($route){
        $this->baseRoute = $route;
        return $this;
    }

    /**
     * Declare intention to build routes for this entity. Will take defaults if not previously defined.
     * @param Entity $entity
     * @param null $callables
     * @param null $patterns
     * @return $this
     */
    protected function addEntityRoutes(Entity $entity, $callables = NULL, $patterns = []){
        $this->setEntityPatterns($entity, $patterns);

        foreach($this->entityPatterns[$entity->getEntityName()] as $verb => $pattern){
            $this->setEntityCallable($entity, $verb, ($callables[$verb] ?? Controller::class.':'.$verb));
        }

        $this->buildEntityRoutes($entity);

        foreach($this->entityPatterns[$entity->getEntityName()] as $verb => $pattern){
            //pass the instance of the entity to the route so we dont have to reload it.
            $this->route::addRouteArg($entity->getEntityName().'.'.strtolower($verb), 'entity', get_class($entity));
        }

        return $this;
    }

    /**
     * @param $entity
     * @param array $patterns
     * @return $this
     */
    protected function setEntityPatterns($entity, array $patterns = []){
        $this->defaultEntityPatterns = [
            'get' => '[/{'.$entity->primaryKey.'}]',
            'put' => '[/{'.$entity->primaryKey.'}]',
            'post' => '', //no pattern here as we should POST to the base entity path (by default)
            'delete' => '[/{'.$entity->primaryKey.'}]',
        ];
        $this->patternsOverridden = !empty($patterns);
        $this->entityPatterns[$entity->getEntityName()] = array_merge($this->defaultEntityPatterns, $patterns);
        return $this;
    }

    /**
     * Allows us to set the callables for each entity declared
     * @param $entity
     * @param $callable
     * @return $this
     */
    protected function setEntityCallable($entity, $verb, string $callable){
        $this->entityControllers[$entity->getEntityName()][$verb] = $callable;
        return $this;
    }

    /**
     * Normal structure is /ModuleName/EntityName/{pattern}
     * Build any default entity routes.
     * @param $entity
     * @param bool $rootRouts
     */
    protected function buildEntityRoutes($entity, $rootRouts = FALSE){
        $method = $rootRouts ? 'rootRoute' : 'addRoute';
        foreach($this->entityPatterns[$entity->getEntityName()] as $verb => $pattern){
            $this->$method(
                $verb,
                $entity->getEntityName().'.'.strtolower($verb),
                (str_replace('entity', '', 'entities/'.strtolower($entity->getEntityName()))).$pattern,
                $this->entityControllers[$entity->getEntityName()][$verb]
            );
        }
    }

    /**
     * Method to add a route, allows the module to have a base path configured and for easy overrides.
     * @param $verb
     * @param $name
     * @param string $pattern
     * @param $callable
     * @return Module
     */
    protected function addRoute($verb, $name, string $pattern, $callable){
        $this->route::{strtolower($verb)}($name, rtrim($this->baseRoute, '/').'/'.ltrim($pattern, '/'), $callable);
        $this->addModuleNameToRoute($name);
        return $this;
    }

    /**
     * Method to add a base root, which will ignore all set base paths at this level.
     * @param $verb
     * @param $name
     * @param string $pattern
     * @param $callable
     * @return Module
     */
    protected function rootRoute($verb, $name, string $pattern, $callable){
        $this->route::{strtolower($verb)}($name, '/'.ltrim($pattern, '/'), $callable);
        $this->addModuleNameToRoute($name);
        return $this;
    }

    /**
     * Method called on instantiation to add available routes to SlimRouter.
     */
    public function routes (){}

    /**
     * @return mixed
     */
    public function schema (){
        return (object)[];
    }
    public function install (){
        return !$this->requiresInstall();
    }

    public function getName(){
        return $this->getNameFromNamespace(get_class($this));
    }

    public function getNameFromNamespace($namespace){
        $moduleSpaces = explode('\\', $namespace);
        return str_replace('Module', '', array_pop($moduleSpaces));
    }

    public function uninstall(){

    }

    public function markInstalled($namespace){
        //update the installation status.
        $vault = Vault::getInstance();
        if($this->checkExistsInDB($namespace)){
            $vault->update('modules')->fields(['installed' => 1])->where('namespace', '\\' . $namespace)->execute();
        } else {
            $vault->insert('modules')->fields(
                [
                    'name' => $this->getNameFromNamespace($namespace),
                    'namespace' => '\\'.$namespace,
                    'weight' => 1,
                    'installed' => 1,
                    'lastInstallDate' => time()
                ]
            )->execute();
        }
    }

    /**
     * @return int
     */
    public function apiVersion(){
        return VERSION;
    }

    protected function checkExistsInDB($namespace){
        //we need to check if we know about this module. If we dont then this is an insert.
        //update the installation status.
        $vault = Vault::getInstance();
        $module = $vault->select('modules')->fields()->where('namespace', '\\'.$namespace)->execute()->fetchAssoc();
        return !empty($module);
    }

    public function isInstalled($forceRefresh = FALSE){
        if(!$forceRefresh && !is_null($this->isInstalled)) {
            return $this->isInstalled;
        }

        if(!$this->requiresInstall()){
            $this->isInstalled = TRUE;
            return $this->isInstalled;
        }

        //system is installing? Then there's no DB to connect to, so this one isn't installed.
        if(System::$installing){
            $this->isInstalled = FALSE;
            return $this->isInstalled;
        }
        try {
            //we might not have a DB to connect to, so we need to make sure we catch that.
            $vault = Vault::getInstance();
            $module = $vault->select('modules')->fields()->where('namespace', '\\' . get_class($this))->execute()->fetchAssoc();
            if(isset($module['installed'])){
                $this->isInstalled = $module['installed'];
                return $this->isInstalled;
            }
        } catch (\Exception $e){/*Do Nothing*/}
        $this->isInstalled = FALSE;
        return $this->isInstalled;
    }

    protected function addModuleNameToRoute($name): Module
    {
        //add the declared module name as a route arg for use later.
        Routes::addRouteArg($name, 'moduleName', strtolower($this->getName()));
        return $this;
    }
}