#!/usr/bin/php -d display_errors
<?php
if (file_exists('./config/host.php')) {
    require_once './config/host.php';
}
defined('INST_PATH') || define('INST_PATH', dirname(realpath(__DIR__).'../').'/');
set_include_path(
    '/etc/dumbophp'.PATH_SEPARATOR.
    '/etc/dumbophp/bin'.PATH_SEPARATOR.
    INST_PATH.PATH_SEPARATOR.
    INST_PATH.'bin'.PATH_SEPARATOR.
    INST_PATH.'lib'.PATH_SEPARATOR.
    get_include_path().PATH_SEPARATOR.
    PEAR_EXTENSION_DIR.PATH_SEPARATOR.
    '/windows/dumbophp'.PATH_SEPARATOR.
    '/windows/dumbophp/bin'.PATH_SEPARATOR.
    '/windows/system32/dumbophp'.PATH_SEPARATOR.
    '/windows/system32/dumbophp/bin'.PATH_SEPARATOR.
    INST_PATH.'DumboPHP'
);

class dumboShell{
    private $commands = [
        'autocomplete',
        'help',
        'create',
        'run',
        'init',
        'db',
        'generate',
        'destroy',
        'migration'
    ];
    private $_options = [
        'env' => ['value' => null, 'cast' => 'string'],
        'halt' => ['value' => false, 'cast' => 'boolean'],
        'standalone' => ['value' => true, 'cast' => 'boolean'],
        'dir' => ['value' => null, 'cast' => 'string'],
        'watch' => ['value' => false, 'cast' => 'boolean']
    ];
    private $command = null;
    private $systemFolder = '';
    private $dumboSource = 'dumbophp/src';
    private $dumboSystemPath = 'dumbophp';
    private $dumboLibs = 'dumbophp/lib';
    private $binPath = '/usr/bin';
    private $fullPathTarget = '';
    private $arguments = [];
    private $params = [];
    private $colors = null;

    public $dumboBin = '';

    public function __construct() {

        $this->systemFolder = dirname(realpath(__DIR__)).'/';

        $this->dumboSource = $this->systemFolder . 'src';
        $this->dumboSystemPath = $this->systemFolder;
        $this->dumboLibs = $this->systemFolder . 'lib';
        $this->dumboBin = $this->systemFolder . 'bin';

        require_once "{$this->dumboLibs}/DumboShellColors.php";
        $this->colors = new DumboShellColors();
    }

    private function _parseOptions() {
        $trueFalse = ['true' => true, 'false' => false];
        foreach($this->arguments as $i => $arg) {
            preg_match('@\-\-([a-zA-Z0-9]+)\=([a-z0-9\-\_\/]+)[\s]*@im', $arg, $match);
            if (sizeof($match) === 3) {
                if(isset($this->_options[$match[1]])){
                    switch($this->_options[$match[1]]['cast']) {
                        case 'numeric':
                            $match[2] = (integer)$match[2];
                        break;
                        case 'boolean':
                            $match[2] = $trueFalse[strtolower($match[2])];
                        break;
                        case 'string':
                            $match[2] = trim((string)$match[2]);
                        break;
                        default:
                            throw new Exception("Value not allowed for {$match[1]}");
                        break;
                    }
                    $this->_options[$match[1]]['value'] = strlen($match[2]) > 0 ? $match[2] : null;
                }
                $this->arguments[$i] = null;
                unset($this->arguments[$i]);
            }
        }
    }

    public function showError($errorMessage) {
        fwrite(STDOUT, $this->colors->getColoredString($errorMessage, "white", "red") . "\n");
    }

    public function showMessage($errorMessage) {
        fwrite(STDOUT, $this->colors->getColoredString($errorMessage, "white", "green") . "\n");
    }

    public function showNotice($errorMessage) {
        fwrite(STDOUT, $this->colors->getColoredString($errorMessage, "blue", "yellow") . "\n");
    }

    public function validateApacheConf() {
        $modsRequired = [
            'mod_rewrite'
        ];
    }

    public function run($argv) {
        if(empty($argv[1]) || sizeof($argv) < 2) {
            $this->help();
            die($this->showError('Error: Option not valid.'));
        }

        array_shift($argv);
        $this->command = array_shift($argv);
        $this->arguments = $argv;
        $this->_parseOptions();

        if(in_array($this->command, $this->commands)){
            switch($this->command) {
                case 'create':
                    $this->createSite();
                break;
                case 'generate':
                    $this->generateScripts();
                break;
                case 'destroy':
                    $this->destroyScripts();
                break;
                case 'db':
                    $this->dbScripts();
                break;
                case 'migration':
                    $this->migrationScripts();
                break;
                case 'init':
                    $this->initAppScript();
                break;
                case 'run':
                    $this->runActionScript();
                break;
                case 'autocomplete':
                    $this->_autocomplete();
                break;
                case 'help':
                default:
                    $this->help();
                break;
            }
        } else {
            $this->help();
        }

    }

    private function _getControllers() {
        file_exists('./config/host.php') or die();
        require_once './config/host.php';
        $controllersPath = INST_PATH.'app/controllers';
        $controllersDir = dir($controllersPath);
        $controllersList = [];
        while (($file = $controllersDir->read()) != false):
            if($file != "." and $file != ".." and preg_match('/(.+)_controller\.php/', $file, $matches) === 1):
                $file = preg_replace('/(_controller\.php)/', '', $file);
                $controllersList[] = $file;
            endif;
        endwhile;

        return $controllersList;
    }

    private function _getActions($controller) {
        file_exists('./config/host.php') or die();
        require_once './config/host.php';
        require_once "{$this->dumboBin}/dumbophp.php";

        $controllerFile = INST_PATH."app/controllers/{$controller}_controller.php";
        file_exists($controllerFile) or die();
        $className = ucfirst("{$controller}Controller");
        $actions = [];

        require_once $controllerFile;
        $methods = get_class_methods($className);
        while(null !== ($method = array_shift($methods))):
            if(preg_match('/^[^_](.+)Action/', $method)):
                $actions[] = str_replace('Action', '', $method);
            endif;
        endwhile;

        return $actions;
    }

    private function _getFullActions() {
        $list = [];
        $controllers = $this->_getControllers();
        while(null !== ($controller = array_shift($controllers))):
            $actions = $this->_getActions($controller);
            while(null !== ($action = array_shift($actions))):
                $list[] = "{$controller}/{$action}";
            endwhile;
        endwhile;

        return $list;
    }

    private function _autocomplete() {
        array_shift($this->commands);
        $output = implode("\n", $this->commands);

        if(!empty($this->arguments[0]) and in_array($this->arguments[0], $this->commands)):
            switch ($this->arguments[0]):
                case 'run':
                    $list = $this->_getFullActions();
                    $output = implode(' ', $list);
                break;
                case 'generate':
                    $list = ['scaffold', 'controller', 'model', 'seed'];
                    $output = implode(' ', $list);
                break;
                case 'destroy':
                    $list = ['scaffold', 'model'];
                    $output = implode(' ', $list);
                break;
                case 'migration':
                    $list = ['up', 'down', 'reset', 'run', 'sow'];
                    $output = implode(' ', $list);
                break;
                case 'db':
                    $list = ['dump', 'load'];
                    $output = implode(' ', $list);
                break;
            endswitch;
        endif;

        echo $output;
    }

    private function help() {
        $text = <<<DUMBO
▓█████▄  █    ██  ███▄ ▄███▓ ▄▄▄▄    ▒█████
▒██▀ ██▌ ██  ▓██▒▓██▒▀█▀ ██▒▓█████▄ ▒██▒  ██▒
░██   █▌▓██  ▒██░▓██    ▓██░▒██▒ ▄██▒██░  ██▒
░▓█▄   ▌▓▓█  ░██░▒██    ▒██ ▒██░█▀  ▒██   ██░
░▒████▓ ▒▒█████▓ ▒██▒   ░██▒░▓█  ▀█▓░ ████▓▒░
 ▒▒▓  ▒ ░▒▓▒ ▒ ▒ ░ ▒░   ░  ░░▒▓███▀▒░ ▒░▒░▒░
 ░ ▒  ▒ ░░▒░ ░ ░ ░  ░      ░▒░▒   ░   ░ ▒ ▒░
 ░ ░  ░  ░░░ ░ ░ ░      ░    ░    ░ ░ ░ ░ ▒
   ░       ░            ░    ░          ░ ░
 ░                                ░

DumboPHP 2.0 by Rantes
DumboPHP shell.
Ussage:

    dumbo <command> <option> <params>

Commands:

    create <project-name>
        Creates a new site. Param: site name.

    init [--standalone=[true|false]]
        Initializes the project to use DumboPHP.

    generate [scaffold|controller|model|seed] <name>
        Generates scripts for model, controller or scaffold.

    destroy [scaffold|model] <name>
        Generates scripts for model, controller or scaffold.

    migration [up|down|reset|run|sow] <migration>
        Executes migrations actions.

    db [dump|load] <all|model>
        Actions for database.

    run [controller/action] [<param=val> <paramn=valn>]
        executes a controller/action. index/index as default.

Options:

    --env=enviroment        Sets a particular enviroment for the execution
    --halt=[true|false]     Halt the script on error
    --watch                 Set a daemon to watch files (used in tests)

DUMBO;
        fwrite(STDOUT, $text . "\n");
    }

    private function _copyInstallFiles() {
        $target = $this->arguments[0] ?? '.';
        $d = dir($target);
        $this->fullPathTarget = realpath($d->path);
        $d->close();

        $actions = array(
            'Creating directory: '.$this->fullPathTarget.'/app' =>'/app',
            'Creating directory: '.$this->fullPathTarget.'/app/controllers' => '/app/controllers',
            'Creating directory: '.$this->fullPathTarget.'/app/helpers' =>'/app/helpers',
            'Creating directory: '.$this->fullPathTarget.'/app/models' =>'/app/models',
            'Creating directory: '.$this->fullPathTarget.'/app/views' =>'/app/views',
            'Creating directory: '.$this->fullPathTarget.'/app/webroot' =>'/app/webroot',
            'Creating directory: '.$this->fullPathTarget.'/app/webroot/css' =>'/app/webroot/css',
            'Creating directory: '.$this->fullPathTarget.'/app/webroot/fonts' =>'/app/webroot/fonts',
            'Creating directory: '.$this->fullPathTarget.'/app/webroot/images' =>'/app/webroot/images',
            'Creating directory: '.$this->fullPathTarget.'/app/webroot/js' =>'/app/webroot/js',
            'Creating directory: '.$this->fullPathTarget.'/app/webroot/plugins' =>'/app/webroot/plugins',
            'Creating directory: '.$this->fullPathTarget.'/config' =>'/config',
            'Creating directory: '.$this->fullPathTarget.'/migrations' =>'/migrations'
        );

        foreach($actions as $copy => $action){
            $this->showMessage('Running task: '.$copy);
            mkdir($this->fullPathTarget.$action) or die($this->showError('Error on building: Cannot write on destination folder. Exiting.'));
        }

        $actions = array(
            'Creating file system: Main .htaccess' => array($this->dumboSource.'/main.htaccess', $this->fullPathTarget.'/.htaccess'),
            'Creating file system: Main .env.example' => array($this->dumboSource.'/main.env.example', $this->fullPathTarget.'/.env.example'),
            'Creating file system: Webroot .htaccess' => array($this->dumboSource.'/webroot.htaccess', $this->fullPathTarget.'/app/webroot/.htaccess'),
            'Creating file system: favicon' => array($this->dumboSource.'/favicon.ico', $this->fullPathTarget.'/app/webroot/favicon.ico'),
            'Creating file system: config/db' => array($this->dumboSource.'/db_settings.php', $this->fullPathTarget.'/config/db_settings.php'),
            'Creating file system: config/host' => array($this->dumboSource.'/host.php', $this->fullPathTarget.'/config/host.php'),
            'Creating file system: config/index' => array($this->dumboSource.'/index.php', $this->fullPathTarget.'/app/webroot/index.php'),
            'Creating file system: layout' => array($this->dumboSource.'/layout.phtml', $this->fullPathTarget.'/app/views/layout.phtml')
        );

        reset($actions);

        foreach($actions as $copy => $action){
            $this->showMessage('Running task: '.$copy);
            copy($action[0], $action[1]) or die($this->showError('Error on building: Cannot write on destination folder. Exiting.'));
        }

        if(!empty($this->options['standalone'])) {
            $this->showMessage('Building standalone site.');
            copy($this->dumboSource.'/dumbophp.php',$this->fullPathTarget.'/dumbophp.php') or die($this->showError('Error on building: Cannot write on destination folder. Exiting.'));
        }
    }

    private function initAppScript() {
        $this->showNotice('Initializing DumboPHP project. ');

        $this->_copyInstallFiles();

        if (!empty($this->arguments[1]) && $this->arguments[1] === 'standalone') {
            $this->showMessage('Building standalone site.');
            copy($this->dumboSource.'/dumbophp.php',INST_PATH.'dumbophp.php') or die($this->showError('Error on building: Cannot write on destination folder. Exiting.'));
        }

        $this->showNotice('Build complete.');
    }

    private function createSite() {
        empty($this->arguments[0]) && die($this->showError('Error: Creation aborted. Project name must be provided.'));
        $this->showNotice('Creating site named: "'.$this->arguments[0].'"');
        if(!file_exists($this->arguments[0])) {
            mkdir($this->arguments[0]);
        } else {
            die($this->showError('Error: Creation aborted. Project folder exists already.'));
        }

        $this->_copyInstallFiles();

        $this->showNotice('Build complete.');
    }

    private function generateScripts() {
        if(empty($this->arguments[0]) || $this->arguments[0] !== 'seed' && sizeof($this->arguments) < 2) {
            $this->help();
            die($this->showError('Error: Missing params.'));
        }

        for ($i=1; $i < sizeof($this->arguments); $i++) {
            $this->params[] = $this->arguments[$i];
        }

        require_once "{$this->dumboLibs}/generator.php";
        $generator = new DumboGeneratorClass($this->_options['env']['value']);

        switch ($this->arguments[0]) {
            case 'scaffold':
                $this->showNotice('Creating scaffold for "'.$this->arguments[1].'".');
                $generator->scaffold($this->params);
            break;
            case 'controller':
                $this->showNotice('Creating controller: "'.$this->arguments[1].'".');
                $generator->controller($this->params);
                $generator->views($this->params);
            break;
            case 'model':
                $this->showNotice('Creating model: "'.$this->arguments[1].'".');
                $generator->model($this->params);
            break;
            case 'seed':
                $this->showNotice('Creating seed file...');
                $generator->seed();
            break;

            default:
                $this->help();
                die($this->showError('Option no valid for generate.'));
            break;
        }
    }

    private function destroyScripts() {
        if(empty($this->arguments[0]) or empty($this->arguments[1])) {
            $this->help();
            die($this->showError('Error: Missing params.'));
        }

        if(sizeof($this->arguments) > 2) {
            $this->help();
            die($this->showError('Error: Only one model for delete at once.'));
        }

        file_exists('./config/host.php') or die($this->showError('Destroy actions must be executed at the top level of project path.'.PHP_EOL));

        require_once './config/host.php';
        require_once "{$this->dumboBin}/dumbophp.php";

        empty($this->_options['env']['value']) || ($GLOBALS['env'] = $this->_options['env']['value']);

        switch ($this->arguments[0]) {
            case 'scaffold':
                $this->showNotice('Deleting scaffold for "'.$this->arguments[1].'".');
                $singular = singulars($this->arguments[1]);
                $model = INST_PATH."app/models/{$singular}.php";
                $migration = INST_PATH."migrations/create_{$this->arguments[1]}.php";
                $controller = INST_PATH."app/controllers/{$singular}_controller.php";
                $views = INST_PATH."app/views/{$singular}";

                $this->showNotice("Deleting model: {$model}");
                is_file($model) && unlink($model);

                $this->showNotice("Deleting migration: {$this->arguments[1]}");
                is_file($migration) && unlink($migration);

                $this->showNotice("Deleting controller: {$controller}");
                is_file($controller) && unlink($controller);

                if (is_dir($views)):
                    $dir = opendir($views);
                    while($file = readdir($dir)):
                           if ($file != "." && $file != ".."):
                                $this->showNotice("Deleting view: {$file}");
                                unlink("{$views}/{$file}");
                           endif;
                     endwhile;
                     closedir($dir);
                     $this->showNotice("Deleting views folder: {$views}");
                     rmdir($views);
                endif;
            break;
            case 'model':
                $model = INST_PATH.'app/models/'.singulars($this->arguments[1]).'.php';
                $migration = INST_PATH.'migrations/create_'.$this->arguments[1].'.php';
                $this->showNotice('Deleting model: "'.$model.'".');
                file_exists($model) or die($this->showError('Model file does not exists.'.PHP_EOL));
                unlink($model);
                $this->showNotice('Deleting migration: "'.$migration.'".');
                file_exists($migration) or die($this->showError('Migration file does not exists.'.PHP_EOL));
                unlink($migration);
            break;
            /**
             * @todo script for remove controller and views
             */
            case 'controller':
                $controller = INST_PATH.'app/controllers/'.$this->arguments[1].'_controller.php';
                $this->showNotice('Deleting controller: "'.$controller.'".');
                file_exists($controller) or die($this->showError('controller file does not exists.'.PHP_EOL));
                unlink($controller);
            break;
            default:
                $this->help();
                die($this->showError('Option no valid for generate.'));
            break;
        }
    }

    private function dbScripts() {
        if(empty($this->arguments[0]) or empty($this->arguments[1])) {
            $this->help();
            die($this->showError('Error: Missing params.'));
        }

        file_exists('./config/host.php') or die($this->showError('DB actions must be executed at the top level of project path.'.PHP_EOL));

        require_once './config/host.php';
        require "{$this->dumboBin}/dumbophp.php";

        empty($this->_options['env']['value']) || ($GLOBALS['env'] = $this->_options['env']['value']);

        $modelsPath = INST_PATH.'app/models/';
        $models = array();

        if ($this->arguments[1] === 'all') {
            $modelsDir = dir($modelsPath);
            while (($file = $modelsDir->read()) != FALSE) {
                if($file != "." and $file != ".." and preg_match('/(.+)\.php/', $file, $matches) === 1) {
                    $models[] = array('file'=>$matches[0],'model'=>$matches[1],'class'=>Camelize($matches[1]));
                }
            }
        } else {
            for ($i=1; $i < sizeof($this->arguments); $i++) {
                $name = Singulars($this->arguments[$i]);
                if (file_exists($modelsPath.$name.'.php')) {
                    $models[] = array('file'=>$name.'.php','model'=>$name,'class'=>Camelize($name));
                } else {
                    $this->showError('Model not found: '.$this->arguments[$i]);
                }
            }
        }

        switch ($this->arguments[0]) {
            case 'load':
                if (!empty($models)) {
                    foreach ($models as $model) {
                        $this->showNotice('Loading data for the model: "'.$model['model'].'".');
                        require_once $modelsPath.$model['file'];
                        $obj = new $model['class']();
                        $obj->LoadDump();
                    }
                }
            break;
            case 'dump':
                if (!empty($models)) {
                    foreach ($models as $model) {
                        $this->showNotice('Exporting data for the model: "'.$model['model'].'".');
                        require_once $modelsPath.$model['file'];
                        $obj = new $model['class']();
                        $data = $obj->Find();
                        $data->Dump();
                    }
                }
            break;
            default:
                $this->help();
                die($this->showError('Error: Option no valid.'));
            break;
        }
    }

    private function runActionScript() {
        empty($_SERVER['REQUEST_METHOD']) and ($_SERVER['REQUEST_METHOD'] = 'GET');
        $_GET['url'] = empty($this->arguments[0])? 'index/index' : $this->arguments[0];
        array_shift($this->arguments);
        while(null !== ($arg = array_shift($this->arguments))){
            $param = explode('=', $arg);
            sizeof($param) === 2 and ($_GET[urldecode($param[0])] = urldecode($param[1]));
        }

        require_once('app/webroot/index.php');
    }

    private function migrationScripts() {
        file_exists('./config/host.php') or die($this->showError('Migration actions must be executed at the top level of project path.'.PHP_EOL));

        require_once './config/host.php';
        require_once "{$this->dumboBin}/dumbophp.php";

        for ($i=1; $i < sizeof($this->arguments); $i++) {
            $this->params[] = $this->arguments[$i];
        }

        empty($this->arguments[0]) && die($this->showError('Error: Not enough arguments; the migrations to affect must be defined.'));

        ($this->arguments[0] === 'sow' || sizeof($this->params) > 0) or die($this->showError('Error: Not enough arguments; the migrations to affect must be defined.'));

        empty($this->_options['env']['value']) || ($GLOBALS['env'] = $this->_options['env']['value']);
        $migrationsPath = INST_PATH.'migrations/';
        $objects = [];

        if($this->arguments[0] !== 'sow') {
            if(sizeof($this->params) === 1 and $this->params[0] === 'all') {
                $migrationsDir = dir($migrationsPath);
                while (($file = $migrationsDir->read()) != false) {
                    if($file != "." and $file != ".." and preg_match('/create_(.+)\.php/', $file, $matches) === 1) {
                        echo PHP_EOL, 'Running action ', $this->arguments[0], ' for: ', $matches[1], PHP_EOL;
                        require_once $migrationsPath.$matches[0];
                        $class = 'Create'.Camelize(Singulars($matches[1]));
                        $objects[] = new $class();
                    }
                }
            } else {
                foreach ($this->params as $migration) {
                    $file = $migrationsPath.'create_'.$migration.'.php';
                    file_exists($file) or die('Migration file '.$migration.', does not exists.'.PHP_EOL);
                    echo PHP_EOL, 'Running action ', $this->arguments[0], ' for: ', $migration, PHP_EOL;
                    require_once $file;
                    $class = 'Create'.Camelize(Singulars($migration));
                    $objects[] = new $class();
                }
            }
        }

        switch ($this->arguments[0]) {
            case 'sow':
                $this->showNotice('Sowing the seeds of this project...');
                file_exists($migrationsPath.'seeds.php')  or die($this->showError('Error: No seeds file exists.'));
                require_once $migrationsPath.'seeds.php';
                $Seeds = new Seed();
                $Seeds->sow();
            break;
            case 'reset':
                foreach($objects as $obj) {
                    $obj->down();
                    $obj->up();
                }
            break;
            default:
                foreach($objects as $obj) {
                    $obj->{$this->arguments[0]}();
                }
            break;
        }
    }
}

$shell = new dumboShell();
$shell->run($argv);
?>
