<?php
    
    /**
     * Class Initializer
     *
     * Class to initialize Artbox application
     */
    class Initializer
    {
        private $params = [];
        private $root;
        private $envs = [];
        private $environment;
        private $db = [];
        private static $instance;
        
        const VERSION = '1.0';
        const CALLBACKS = [
            'setCookieValidationKey',
            'setWritable',
            'setExecutable',
            'createSymlink',
        ];
        
        /**
         * Initialize application
         */
        public static function initialize()
        {
            $instance = self::getInstance();
            $instance->defineEnvironment();
            $instance->startInitialization();
            if ($instance->askInitDb()) {
                do {
                    $instance->defineDb();
                } while (!$instance->databaseCheck());
                $instance->migrate();
                $instance->createUser();
                $instance->congratulate();
            }
        }
        
        /**
         * Get instance of Initializer
         *
         * @return mixed
         */
        public static function getInstance(): Initializer
        {
            if (empty( self::$instance )) {
                self::$instance = new self();
            }
            return self::$instance;
        }
        
        /**
         * Initializer private constructor. You can get Singleton with getInstance()
         *
         * Availiable options:
         * * --env              - One of available environments
         * * --overwrite        - Do overwrite all files without confirmation
         * * --dbinit           - Initialize database after app initialization
         * * --dbtype           - Database type. Available: postgresql, mysql
         * * --host             - Database host, default to 127.0.0.1
         * * --port             - Database port used by postgresql, default to 5432
         * * --schema           - Database schema for postresql, default to public
         * * --dbname           - Database name, required
         * * --username         - Database username, required
         * * --password         - Database password, required
         * * --migrationPath    - Migration path, default to vendor/artweb/artbox-core/migrations
         * * --migrate          - Whether to migrate, default to apply, set no to skip migration
         * * --user_username    - Username for user creation
         * * --user_email       - Email for user creation
         * * --user_password    - Password for user creation
         * * --user             - Whether to create user, default to yes, set no to skip creation
         * * --defaultuser      - Whether to use default user creation
         * * --o                - Webpage to open after intallaction process
         * * --fun              - Run HOLLYWOOD BABY!!!
         * * --phppath          - Path to execute php script
         *
         * @see Initializer::getInstance()
         */
        private function __construct()
        {
            echo $this->formatMessage(
                    "\tArtbox Application Initialization Tool v" . self::VERSION,
                    [ 'bold' ]
                ) . "\n\n";
        }
        
        /**
         * Define environment for an application
         *
         * @return string
         */
        private function defineEnvironment()
        {
            $envName = $this->getParamValue('env');
            $envs = $this->getEnvironmentNames();
            if (empty( $envName ) || $envName === '1') {
                do {
                    $envName = $this->askEnvironment($envs);
                    echo "\n  Initialize the application under '{$envName}' environment? [yes|no] ";
                    $answer = trim(fgets(STDIN));
                } while (strncasecmp($answer, 'y', 1));
            } else {
                if (!in_array($envName, $envs)) {
                    $this->printError("Wrong environment name, available list: " . implode(', ', $envs));
                    exit( 1 );
                }
            }
            $this->environment = $this->envs[ $envName ];
            return $this->environment;
        }
        
        /**
         * Start actual application initialization with files overwriting
         */
        private function startInitialization()
        {
            echo "\n  Start initialization\n";
            echo "  =====================\n";
            
            $this->rewriteFiles();
            $this->callback();
            echo "\n  App initialization completed\n";
            echo "  ==============================\n";
        }
        
        /**
         * Ask whether to init databse. If dbinit param is set, this step will be skipped.
         *
         * @return bool
         */
        private function askInitDb(): bool
        {
            echo "  Start database initialization\n";
            $params = $this->getParams();
            if (!isset( $params[ 'dbinit' ] )) {
                do {
                    echo $this->formatMessage("\n  Init the database? [yes|no] ", [ 'bold' ]);
                    $answer = trim(fgets(STDIN));
                } while (( strncasecmp($answer, 'y', 1) !== 0 ) && ( strncasecmp($answer, 'n', 1) !== 0 ));
                if (strncasecmp($answer, 'n', 1) == 0) {
                    $this->noDatabaseMessage();
                }
            }
            return true;
        }
        
        /**
         * Define which database driver to use. Available options: postgresql, mysql
         */
        private function defineDb()
        {
            $params = $this->getParams();
            if (isset( $params[ 'dbinit' ] )) {
                $this->validateConnection();
            } else {
                $answer = '';
                do {
                    if (!empty( $answer )) {
                        $this->printError("Incorrect database type. Try again or write 'quit' to exit");
                    }
                    echo "\n  Which database do you want to use? [postgresql|mysql]  ";
                    $answer = trim(fgets(STDIN));
                    
                } while (( strncasecmp($answer, 'q', 1) !== 0 ) && ( !in_array(
                        $answer,
                        [
                            'postgresql',
                            'mysql',
                            'p',
                            'm',
                        ]
                    ) ));
                if (strncasecmp($answer, 'q', 1) === 0) {
                    $this->noDatabaseMessage();
                }
                if (strncasecmp($answer, 'p', 1) === 0) {
                    $answer = 'postgresql';
                } else {
                    $answer = 'mysql';
                }
                if ($answer == 'postgresql') {
                    $this->initPostgres();
                } else {
                    $this->initMysql();
                }
            }
            $this->initDb();
        }
        
        /**
         * Get parsed params
         *
         * @see Initializer::parseParams()
         *
         * @return array
         */
        private function getParams(): array
        {
            if (empty( $this->params )) {
                $this->parseParams();
            }
            return $this->params;
        }
        
        /**
         * Get params from input string
         * For example
         *
         * **<code>php init test --param1=value1 --param2=value2</code>**
         *
         * will get
         *
         * <code>**
         * [
         *  test,
         *  value1,
         *  value2
         * ]
         * </code>
         *
         * @return array
         */
        private function parseParams(): array
        {
            $rawParams = [];
            if (isset( $_SERVER[ 'argv' ] )) {
                $rawParams = $_SERVER[ 'argv' ];
                array_shift($rawParams);
            }
            
            $params = [];
            foreach ($rawParams as $param) {
                if (preg_match('/^--(\w+)(=(.*))?$/', $param, $matches)) {
                    $name = $matches[ 1 ];
                    $params[ $name ] = isset( $matches[ 3 ] ) ? $matches[ 3 ] : true;
                } else {
                    $params[] = $param;
                }
            }
            $this->params = $params;
            return $this->params;
        }
        
        /**
         * Get value of input param. Empty string if not exist.
         *
         * @param string $name
         *
         * @return string
         */
        private function getParamValue(string $name): string
        {
            $params = $this->getParams();
            if (isset( $params[ $name ] ) && !empty( $params[ $name ] )) {
                return $params[ $name ];
            } else {
                return '';
            }
        }
        
        /**
         * Get project root directory according to file system
         *
         * @return mixed
         */
        private function getRoot(): string
        {
            if (empty( $this->root )) {
                $this->root = str_replace('\\', '/', __DIR__);
            }
            return $this->root;
        }
        
        /**
         * Get available environments from environments/index.php file
         *
         * Follow environments/index.php manifest to create your own environments
         *
         * @return array
         */
        private function getEnvironments(): array
        {
            $path = "{$this->getRoot()}/environments/index.php";
            if (empty( $this->envs )) {
                /** @noinspection PhpIncludeInspection */
                $this->envs = require( $path );
            }
            if (empty( $this->envs )) {
                die( "There are no available environments in $path" );
            }
            return $this->envs;
        }
        
        /**
         * Get environment names
         *
         * @return array
         */
        private function getEnvironmentNames(): array
        {
            return array_keys($this->getEnvironments());
        }
        
        /**
         * Show user variants to choose environment from
         *
         * @param array $envs
         *
         * @return string
         */
        private function askEnvironment(array $envs): string
        {
            echo "Which environment do you want the application to be initialized in?\n\n";
            foreach ($envs as $i => $name) {
                echo "  [$i] $name\n";
            }
            $answer = $this->offerEnvironment(count($envs));
            while (!( ( ctype_digit($answer) && in_array($answer, range(0, count($envs) - 1)) ) || $answer === 'q' )) {
                $answer = $this->offerEnvironment(count($envs));
            }
            if ($answer === 'q') {
                echo "\n Quit initialization.\n";
                exit( 0 );
            } else {
                if (isset( $envs[ $answer ] )) {
                    $envName = $envs[ $answer ];
                } else {
                    die( "Error while trying to get environment name. Try another one." );
                }
            }
            return $envName;
        }
        
        /**
         * Offer user to choose environment number
         *
         * @param int $count
         *
         * @return string
         */
        private function offerEnvironment(int $count): string
        {
            echo "\n  Your choice [0-" . ( $count - 1 ) . ', or "q" to quit] ';
            return trim(fgets(STDIN));
        }
        
        /**
         * Rewrite files by files in <code>environments/$environment['path']</code>
         */
        private function rewriteFiles()
        {
            $environment = $this->environment;
            $root = $this->getRoot();
            if (isset( $environment[ 'path' ] ) && !empty( $environment[ 'path' ] )) {
                $path = $environment[ 'path' ];
            } else {
                $this->printError('Environment configuration failed. Please set path value.');
                exit( 1 );
            }
            $files = $this->getFileList("$root/environments/$path");
            if (isset( $environment[ 'skipFiles' ] )) {
                $skipFiles = $environment[ 'skipFiles' ];
                array_walk(
                    $skipFiles,
                    function (&$value) use ($root) {
                        $value = "$root/$value";
                    }
                );
                $files = array_diff(
                    $files,
                    array_intersect_key(
                        $environment[ 'skipFiles' ],
                        array_filter($skipFiles, 'file_exists')
                    )
                );
            }
            $all = false;
            foreach ($files as $file) {
                if (!$this->copyFile("environments/{$path}/$file", $file, $all)) {
                    break;
                }
            }
        }
        
        /**
         * Recursively get all files from $root directory.
         *
         * This will ignore .git, .svn directories.
         *
         * @param string $root
         * @param string $basePath
         *
         * @return array
         */
        private function getFileList(string $root, string $basePath = ''): array
        {
            $files = [];
            $handle = opendir($root);
            while (( $path = readdir($handle) ) !== false) {
                if ($path === '.git' || $path === '.svn' || $path === '.' || $path === '..') {
                    continue;
                }
                $fullPath = "$root/$path";
                $relativePath = $basePath === '' ? $path : "$basePath/$path";
                if (is_dir($fullPath)) {
                    $files = array_merge($files, $this->getFileList($fullPath, $relativePath));
                } else {
                    $files[] = $relativePath;
                }
            }
            closedir($handle);
            return $files;
        }
        
        /**
         * Run callbacks for application initialization
         */
        private function callback()
        {
            $environment = $this->environment;
            foreach (self::CALLBACKS as $callback) {
                if (!empty( $environment[ $callback ] )) {
                    $this->$callback($environment[ $callback ]);
                }
            }
        }
        
        /**
         * Set directories in $environment['setWrotable'] to chmod 0777
         *
         * @param $paths
         */
        private function setWritable(array $paths)
        {
            $root = $this->getRoot();
            foreach ($paths as $writable) {
                $fullPath = "$root/$writable";
                if (is_dir($fullPath)) {
                    if (@chmod($fullPath, 0777)) {
                        echo $this->formatMessage(
                                "  ***** Set writable $writable (chmod 0777)",
                                [ 'fg-yellow' ]
                            ) . "\n";
                    } else {
                        $this->printError("Operation chmod not permitted for directory $writable.");
                    }
                } else {
                    if (!@mkdir($fullPath, 0777, true)) {
                        $this->printError("Directory $writable does not exist and cannot be created.");
                    }
                }
            }
        }
        
        /**
         * Set files in $environment['setExecutable'] to chmod 0755
         *
         * @param $paths
         */
        private function setExecutable(array $paths)
        {
            $root = $this->getRoot();
            foreach ($paths as $executable) {
                $fullPath = "$root/$executable";
                if (file_exists($fullPath)) {
                    if (@chmod($fullPath, 0755)) {
                        echo $this->formatMessage("  ***** Set executable $executable (chmod 0755)") . "\n";
                    } else {
                        $this->printError("Operation chmod not permitted for $executable.");
                    }
                } else {
                    $this->printError("$executable does not exist.");
                }
            }
        }
        
        /**
         * Set cookie validation keys to files in $environment['setCookieValidationKey'].
         *
         * @param $paths
         */
        private function setCookieValidationKey(array $paths)
        {
            $root = $this->getRoot();
            foreach ($paths as $file) {
                echo $this->formatMessage(
                        "  ***** Generate cookie validation key in $file",
                        [
                            'bold',
                            'fg-magenta',
                        ]
                    ) . "\n";
                $file = $root . '/' . $file;
                $length = 32;
                $bytes = openssl_random_pseudo_bytes($length);
                $key = strtr(substr(base64_encode($bytes), 0, $length), '+/=', '_-.');
                $this->setParam('cookieValidationKey', $key, $file);
            }
        }
        
        private function createSymlink(array $links)
        {
            $root = $this->getRoot();
            foreach ($links as $link => $target) {
                //first removing folders to avoid errors if the folder already exists
                @rmdir($root . "/" . $link);
                //next removing existing symlink in order to update the target
                if (is_link($root . "/" . $link)) {
                    @unlink($root . "/" . $link);
                }
                if (@symlink($root . "/" . $target, $root . "/" . $link)) {
                    echo $this->formatMessage("  ***** Symlink $root/$target $root/$link", [ 'fg-blue' ]) . "\n";
                } else {
                    $this->printError("Cannot create symlink $root/$target $root/$link.");
                }
            }
        }
        
        /**
         * Copy file from environment directory to project directory
         *
         * @param string $source Environment source file path
         * @param string $target Project file path target
         * @param bool   $all    Rewrite all flag
         *
         * @return bool
         */
        private function copyFile(string $source, string $target, bool &$all)
        {
            $root = $this->getRoot();
            $params = $this->getParams();
            if (!is_file($root . '/' . $source)) {
                echo $this->formatMessage("  ----- skip $target ($source not exist)", [ 'fg-cyan' ]) . "\n";
                return true;
            }
            if (is_file($root . '/' . $target)) {
                if (file_get_contents($root . '/' . $source) === file_get_contents($root . '/' . $target)) {
                    echo $this->formatMessage("  ----- unchanged $target", [ 'fg-cyan' ]) . "\n";
                    return true;
                }
                if ($all) {
                    echo $this->formatMessage("  ----- overwrite $target", [ 'fg-blue' ]) . "\n";
                } else {
                    echo $this->formatMessage("  -- exist $target", [ 'fg-magenta' ]) . "\n";
                    echo "         overwrite? [Yes|No|All|Quit] ";
                    
                    $answer = !empty( $params[ 'overwrite' ] ) ? $params[ 'overwrite' ] : trim(fgets(STDIN));
                    echo "\n";
                    if (!strncasecmp($answer, 'q', 1)) {
                        return false;
                    } else {
                        if (!strncasecmp($answer, 'y', 1)) {
                            echo $this->formatMessage("  ----- overwrite $target", [ 'fg-blue' ]) . "\n";
                        } else {
                            if (!strncasecmp($answer, 'a', 1)) {
                                echo $this->formatMessage("  ----- overwrite $target", [ 'fg-blue' ]) . "\n";
                                $all = true;
                            } else {
                                echo $this->formatMessage(
                                        "  ----- skip $target ($source not exist)",
                                        [ 'fg-cyan' ]
                                    ) . "\n";
                                return true;
                            }
                        }
                    }
                }
                file_put_contents($root . '/' . $target, file_get_contents($root . '/' . $source));
                return true;
            }
            echo "\n" . $this->formatMessage("  ----- generate $target", [ 'fg-green' ]) . "\n";
            @mkdir(dirname($root . '/' . $target), 0777, true);
            file_put_contents($root . '/' . $target, file_get_contents($root . '/' . $source));
            return true;
        }
        
        /**
         * Set param in file to particular value.
         *
         * For example: setParam('param', 'value', 'config.php') will replace content in config.php
         *
         * from
         *
         * 'param' => ''
         *
         * to
         *
         * 'param' => 'value'
         *
         * @param string $param
         * @param string $value
         * @param string $file
         * @param bool   $noQuote
         * @param bool   $force
         * @param bool   $alternate
         */
        private function setParam(
            string $param,
            string $value,
            string $file,
            bool $noQuote = false,
            bool $force = false,
            bool $alternate = false
        ) {
            if ($alternate) {
                $regexp = '/(("|\')db("|\')\s*=>\s*)(.*|.*)/';
            } else {
                if ($force) {
                    $regexp = '/(("|\')' . $param . '("|\')\s*=>\s*)(".*"|\'.*\')/';
                } else {
                    $regexp = '/(("|\')' . $param . '("|\')\s*=>\s*)(""|\'\')/';
                }
            }
            if ($noQuote) {
                $content = preg_replace(
                    $regexp,
                    "\\1$value",
                    file_get_contents($file)
                );
            } else {
                $content = preg_replace(
                    $regexp,
                    "\\1'$value'",
                    file_get_contents($file)
                );
            }
            file_put_contents($file, $content);
        }
        
        /**
         * Init postgres connection
         *
         * @return array
         */
        private function initPostgres(): array
        {
            $db = [
                'dbtype' => 'postgresql',
            ];
            echo "\n  Enter your database host: ";
            $host = trim(fgets(STDIN));
            echo "\n  Enter your database port: ";
            $port = trim(fgets(STDIN));
            echo "\n  Enter your database schema: ";
            $db[ 'schema' ] = trim(fgets(STDIN));
            echo "\n  Enter your database name: ";
            $name = trim(fgets(STDIN));
            echo "\n  Enter your database username: ";
            $db[ 'username' ] = trim(fgets(STDIN));
            echo "\n  Enter your database password: ";
            $db[ 'password' ] = trim(fgets(STDIN));
            $db[ 'dsn' ] = "pgsql:host={$host};port={$port};dbname={$name}";
            $this->db = $db;
            return $db;
        }
        
        /**
         * Init mysql connection
         *
         * @return array
         */
        private function initMysql(): array
        {
            $db = [
                'dbtype' => 'mysql',
            ];
            echo "\n  Enter your database host: ";
            $host = trim(fgets(STDIN));
            echo "\n  Enter your database name: ";
            $name = trim(fgets(STDIN));
            echo "\n  Enter your database username: ";
            $db[ 'username' ] = trim(fgets(STDIN));
            echo "\n  Enter your database password: ";
            $db[ 'password' ] = trim(fgets(STDIN));
            $db[ 'dsn' ] = "mysql:host={$host};dbname={$name}";
            $this->db = $db;
            return $db;
        }
        
        /**
         * Validate non-interactive db data and fill it for further actions.
         *
         * @return array
         */
        private function validateConnection(): array
        {
            $db = [];
            $dbType = strtolower($this->getParamValue('dbtype'));
            if ($dbType !== 'postgresql' && $dbType !== 'mysql') {
                die( "Supported DB types are postgresql and mysql" );
            }
            if ($dbType === 'postgresql') {
                $host = $this->getParamValue('host') ? : '127.0.0.1';
                $port = $this->getParamValue('port') ? : '5432';
                $db[ 'schema' ] = $this->getParamValue('schema') ? : 'public';
                $name = $this->getParamValue('dbname');
                if (empty( $name )) {
                    die( "Database name must be set" );
                }
                $db[ 'dsn' ] = "pgsql:host={$host};port={$port};dbname={$name}";
                if (empty( $username = $this->getParamValue('username') )) {
                    die( "Database username must be set" );
                }
                $db[ 'username' ] = $username;
                if (empty( $password = $this->getParamValue('password') )) {
                    die( "Database password must be set" );
                }
                $db[ 'password' ] = $password;
            } else {
                $host = $this->getParamValue('host') ? : '127.0.0.1';
                $name = $this->getParamValue('dbname');
                if (empty( $name )) {
                    die( "Database name must be set" );
                }
                $db[ 'dsn' ] = "mysql:host={$host};dbname={$name}";
                if (empty( $username = $this->getParamValue('username') )) {
                    die( "Database username must be set" );
                }
                $db[ 'username' ] = $username;
                if (empty( $password = $this->getParamValue('password') )) {
                    die( "Database password must be set" );
                }
                $db[ 'password' ] = $password;
            }
            $db[ 'dbtype' ] = $dbType;
            $this->db = $db;
            return $db;
        }
        
        /**
         * Copy db connection file accroding to choosen driver
         */
        private function initDb()
        {
            if (!empty( $configPath = $this->environment[ 'setDbConnection' ] )) {
                if (preg_match('/(.*)\/.*/', $configPath, $matches)) {
                    $path = $matches[ 1 ];
                } else {
                    $this->printError("Unknown error while trying to init database");
                    exit( 1 );
                }
                $filename = "db{$this->db['dbtype']}.php";
                $all = !empty( $this->getParamValue('overwrite') ) ? false : true;
                $fullpath = "{$path}/{$filename}";
                if ($this->copyFile("environments/{$filename}", $fullpath, $all)) {
                    $this->rewriteDb($fullpath, $configPath);
                }
            }
        }
        
        /**
         * Rewrite params in db config and local config files
         *
         * @param string $path
         * @param string $config
         */
        private function rewriteDb(string $path, string $config)
        {
            $db = $this->db;
            $this->setParam('dsn', $db[ 'dsn' ], $path);
            $this->setParam('username', $db[ 'username' ], $path);
            $this->setParam('password', $db[ 'password' ], $path);
            if ($this->db[ 'dbtype' ] == 'postgresql') {
                $this->setParam('defaultSchema', $db[ 'schema' ], $path);
            }
            $filename = "db{$this->db['dbtype']}.php";
            $this->setParam('db', "require('{$filename}')", $config, true, true);
            $driver = ucfirst($db[ 'dbtype' ]);
            echo $this->formatMessage(
                    "Database access file $path was created and filled with configurations for database driver {$driver}",
                    [ 'fg-green' ]
                ) . "\n";
            echo $this->formatMessage(
                    "Database access file $path was linked to local config file $config",
                    [ 'fg-green' ]
                ) . "\n";
            echo $this->formatMessage("Database Access: ", [ 'bg-yellow' ]) . $this->formatMessage(
                    $this->db[ 'dsn' ],
                    [
                        'bg-yellow',
                        'fg-blue',
                    ]
                ) . $this->formatMessage(', username: ', [ 'bg-yellow' ]) . $this->formatMessage(
                    $this->db[ 'username' ],
                    [
                        'bg-yellow',
                        'fg-blue',
                    ]
                ) . "\n";
            echo "  Database initialization completed\n";
            echo "  =================================\n";
        }
        
        /**
         * Prints error message.
         *
         * @param string $message message
         */
        private function printError(string $message)
        {
            echo "\n  " . $this->formatMessage(
                    "Error. $message",
                    [
                        'fg-red',
                        'bold',
                    ]
                ) . " \n";
        }
        
        /**
         * Returns true if the stream supports colorization. ANSI colors are disabled if not supported by the stream.
         * - windows without ansicon
         * - not tty consoles
         *
         * @return boolean true if the stream supports ANSI colors, otherwise false.
         */
        private function ansiColorsSupported(): bool
        {
            return DIRECTORY_SEPARATOR === '\\' ? getenv('ANSICON') !== false || getenv(
                    'ConEmuANSI'
                ) === 'ON' : function_exists('posix_isatty') && @posix_isatty(STDOUT);
        }
        
        /**
         * Get ANSI code of style.
         *
         * @param string $name style name
         *
         * @return integer ANSI code of style.
         */
        private function getStyleCode(string $name): int
        {
            $styles = [
                'bold'       => 1,
                'fg-black'   => 30,
                'fg-red'     => 31,
                'fg-green'   => 32,
                'fg-yellow'  => 33,
                'fg-blue'    => 34,
                'fg-magenta' => 35,
                'fg-cyan'    => 36,
                'fg-white'   => 37,
                'bg-black'   => 40,
                'bg-red'     => 41,
                'bg-green'   => 42,
                'bg-yellow'  => 43,
                'bg-blue'    => 44,
                'bg-magenta' => 45,
                'bg-cyan'    => 46,
                'bg-white'   => 47,
            ];
            return $styles[ $name ];
        }
        
        /**
         * Formats message using styles if STDOUT supports it.
         *
         * @param string   $message message
         * @param string[] $styles  styles
         *
         * @return string formatted message.
         */
        public function formatMessage(string $message, array $styles = []): string
        {
            if (empty( $styles ) || !$this->ansiColorsSupported()) {
                return $message;
            }
            return sprintf(
                "\x1b[%sm",
                implode(
                    ';',
                    array_map(
                        [
                            $this,
                            'getStyleCode',
                        ],
                        $styles
                    )
                )
            ) . $message . "\x1b[0m";
        }
        
        /**
         * Inform that an application initialized without database. Exit code 0, means success.
         */
        private function noDatabaseMessage()
        {
            echo $this->formatMessage(
                "\n Attention: ",
                [
                    'bold',
                    'fg-red',
                    'bg-yellow',
                ]
            );
            echo $this->formatMessage(
                    "Application initialized without database. Set it manually in {$this->getRoot()}/common/config/main-local.php and run migration",
                    [
                        'bold',
                        'bg-yellow',
                    ]
                ) . "\n";
            exit( 0 );
        }
        
        /**
         * Perform database migration if input param migrate doesn't set to 'no'
         */
        private function migrate()
        {
            $migrate = $this->getParamValue('migrate');
            if ($migrate == 'no') {
                echo $this->formatMessage(
                        "Migration skipped by user. In order to application work correct you will need to run it manually",
                        [
                            'bg-yellow',
                            'bold',
                        ]
                    ) . "\n";
                exit( 0 );
            } elseif ($migrate != 'yes') {
                do {
                    echo "  Do you want to perform database migration? [yes|no]";
                    $answer = trim(fgets(STDIN));
                } while (strncasecmp($answer, 'y', 1) !== 0 && strncasecmp($answer, 'n', 1) !== 0);
                if (strncasecmp($answer, 'n', 1) === 0) {
                    echo $this->formatMessage(
                            "Migration skipped by user. In order to application work correct you will need to run it manually",
                            [
                                'bg-yellow',
                                'bold',
                            ]
                        ) . "\n";
                    exit( 0 );
                }
            }
            echo $this->formatMessage("Migration begins...", [ 'fg-yellow' ]) . "\n";
            $migrationPath = $this->getParamValue('migrationPath');
            if (empty( $migrationPath )) {
                $migrationPath = 'vendor/artweb/artbox-core/migrations';
            }
            $phppath = $this->getParamValue('phppath') ? : 'php';
            $result = exec("$phppath yii migrate --migrationPath=$migrationPath --interactive=0", $output, $return);
            if ($return !== 0) {
                $this->printError("Migration cannot be applied. Run it manually to check the reason");
                exit( 1 );
            }
            $this->writeLine($result);
            foreach ($output as $value) {
                $this->writeLine($value);
            }
            echo $this->formatMessage("Migration ended successfully", [ 'fg-yellow' ]) . "\n";
        }
        
        /**
         * Write line of code followed by new line symbol
         *
         * @param string $string
         */
        private function writeLine(string $string)
        {
            echo $string . "\n";
        }
        
        /**
         * Perform user creation if input param user doesn't set to 'no'
         */
        private function createUser()
        {
            $params = $this->getParams();
            $phppath = $this->getParamValue('phppath') ? : 'php';
            if (!isset( $params[ 'defaultuser' ] )) {
                if ($this->getParamValue('user') == 'no') {
                    echo $this->formatMessage(
                            "User creation skipped by user. Run command 'php yii user/create' manually to create user.",
                            [
                                'bg-yellow',
                                'bold',
                            ]
                        ) . "\n";
                    exit( 0 );
                }
                do {
                    echo "  Do you want to create user? [yes|no]";
                    $answer = trim(fgets(STDIN));
                } while (strncasecmp($answer, 'y', 1) !== 0 && strncasecmp($answer, 'n', 1) !== 0);
                if (strncasecmp($answer, 'n', 1) === 0) {
                    echo $this->formatMessage(
                            "User creation skipped by user. Run command 'php yii user/create' manually to create user.",
                            [
                                'bg-yellow',
                                'bold',
                            ]
                        ) . "\n";
                    exit( 0 );
                }
                echo "\n  Enter username: ";
                $username = trim(fgets(STDIN));
                echo "\n  Enter email: ";
                $email = trim(fgets(STDIN));
                echo "\n  Enter password: ";
                $password = trim(fgets(STDIN));
                echo "\n";
                $result = exec("$phppath yii create/user $username $email $password", $output, $return);
            } else {
                $result = exec("$phppath yii create/user", $output, $return);
            }
            $this->handleUserCreation($result, $output, $return);
        }
        
        /**
         * Handle user creation result
         *
         * @param string $result
         * @param array  $output
         * @param int    $return
         */
        private function handleUserCreation($result, $output, $return)
        {
            if ($return !== 0) {
                $this->printError("User cannot be created. Run 'php yii create/user' manually to check the reason");
                exit( 1 );
            }
            $this->writeLine($result);
            echo $this->formatMessage("User created successfully", [ 'fg-yellow' ]) . "\n";
        }
        
        /**
         * Congratulate with successfull installation and optionally open browser
         */
        private function congratulate()
        {
            echo "\n" . $this->formatMessage(
                    "Congratulations. Artbox Basic has been successfully installed.",
                    [
                        'fg-yellow',
                        'bold',
                    ]
                ) . "\n";
            $url = $this->getParamValue('o');
            if (!empty( $url )) {
                if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
                    shell_exec("explorer '{$url}'");
                } else {
                    shell_exec("sensible-browser {$url} > /dev/null 2>/dev/null &");
                    $params = $this->getParams();
                    if (isset( $params[ 'fun' ] )) {
                        echo shell_exec("hollywood");
                    }
                }
            }
        }
        
        /**
         * Check if database connection could be established
         *
         * @return bool
         * @throws \Exception
         */
        private function databaseCheck(): bool
        {
            $params = $this->getParams();
            if (isset( $params[ 'checkdb' ] )) {
                $configPath = $this->environment[ 'setDbConnection' ];
                if (preg_match('/(.*)\/.*/', $configPath, $matches)) {
                    $path = $matches[ 1 ];
                } else {
                    $this->printError("Unknown error while trying to check connection");
                    exit( 1 );
                }
                $filename = "db{$this->db['dbtype']}.php";
                $fullpath = "{$path}/{$filename}";
                $phppath = $this->getParamValue('phppath') ? : 'php';
                exec("$phppath yii check/connection {$fullpath}", $output, $return);
                if ($return > 0) {
                    if (isset( $params[ 'dbinit' ] )) {
                        throw new Exception("Couldn't connect to the database");
                    }
                    echo $this->printError("Database unavailable. Please choose another database!");
                    return false;
                }
            }
            echo $this->formatMessage("  Database connection successful!", [ 'fg-green' ]) . "\n";
            return true;
        }
    }
    