<?php
/**
 * app/classes/installer.php
 *
 * This class is used for the installation, regulation, tracking, and updating of
 * the application. It handles installing the application, installing and updating
 * models as well as the database, and generating and checking the htaccess file.
 *
 * @version 3.0
 * @author  Joey Kimsey <Joey@thetempusproject.com>
 * @link    https://TheTempusProject.com
 * @license https://opensource.org/licenses/MIT [MIT LICENSE]
 */
namespace TheTempusProject\Classes;

use TheTempusProject\Bedrock\Functions\Check;
use TheTempusProject\Bedrock\Functions\Code;
use TheTempusProject\Bedrock\Functions\Cookie;
use TheTempusProject\Canary\Bin\Canary as Debug;
use TheTempusProject\Hermes\Functions\Redirect;
use TheTempusProject\Hermes\Functions\Route as Routes;
use TheTempusProject\Bedrock\Functions\Session;
use TheTempusProject\Classes\DatabaseModel;
use TheTempusProject\Classes\Plugin;

class Installer {
    const MATRIX_MAP = [
        'installPreferences' => 'preferenceMatrix',
        'installPermissions' => 'permissionMatrix',
        'installConfigs' => 'configMatrix',
        'installTable' => 'databaseMatrix',
        'installResources' => 'resourceMatrix',
    ];

    private $override = false;
    private $status = null;
    private static $installJson = null;
    private static $errors = [];

    public function __construct() {
        Debug::log( 'Installer initialized.' );
        if ( self::$installJson === null ) {
            self::$installJson = self::getJson();
        }
    }

    public function getComposerJson() {
        if ( !file_exists( COMPOSER_JSON_LOCATION ) ) {
            Debug::error( 'No install json found.' );
            return false;
        }
        return json_decode( file_get_contents( COMPOSER_JSON_LOCATION ), true );
    }

    public function getComposerLock() {
        if ( !file_exists( COMPOSER_LOCK_LOCATION ) ) {
            Debug::error( 'No install json found.' );
            return false;
        }
        return json_decode( file_get_contents( COMPOSER_LOCK_LOCATION ), true );
    }

    public static function emptyModule( $type, $folder, $filename = '' ) {
        switch ( $type ) {
            case 'model':
                if ( empty( $filename ) ) {
                    $class = '';
                    $class_name = '';
                    break;
                }
                $class = convertFileNameToModelClass( $filename );
                $class_name = convertFileNameToClassName( $filename );
                break;
            case 'plugin':
                if ( empty( $folder ) ) {
                    $class = '';
                    $class_name = '';
                    break;
                }
                $class = convertFileNameToPluginClass( $folder );
                $class_name = convertFolderToClassName( $folder );
                break;
        }
        if ( empty( $folder ) ) {
            $folder = '';
        }
        $object = (object) [
            'name' => $class_name,
            'class' => $class,
            'version' => '0.0',
            'installedVersion' => '',
            'folder' => $folder,
            'type' => $type,
            'installDate' => '',
            'lastUpdate' => '',
            'installStatus' => INSTALL_STATUS_NOT_INSTALLED,
            'enabled' => false,
            'enabled_txt' => 'no',
        ];
        return $object;
    }

    /**
     * This function automatically attempts to install all models in the
     * specified directory.
     * NOTE: The 'Models/ folder is used by default.
     *
     * @param  string $directory - The directory you wish to install all
     *                             models from.
     * @return boolean
     */
    public function getErrors( $array = true ) {
        if ( $array ) {
            $out = [];
            foreach (self::$errors as $error) {
                if ( ! is_array($error) ) {
                    exit(var_export($error,true));
                }
                $out[] = $error['errorInfo'];
            }
        } else {
            $out = self::$errors;
        }
        return $out;
    }

    private static function getJson() {
        if ( file_exists( INSTALL_JSON_LOCATION ) ) {
            $content = file_get_contents( INSTALL_JSON_LOCATION );
            $json = json_decode( $content, true );
        } else {
            touch( INSTALL_JSON_LOCATION );
            $json = [];
        }
        return $json;
    }

    public static function saveJson() {
        $encodedJson = json_encode( self::$installJson );
        if ( !file_exists( INSTALL_JSON_LOCATION ) ) {
            $content = file_get_contents( $location );
            $json = json_decode( $content, true );
            $fh = fopen( INSTALL_JSON_LOCATION, 'w' );
        }
        $writeSuccess = file_put_contents( INSTALL_JSON_LOCATION, $encodedJson );
        if ( $writeSuccess ) {
            return true;
        }
        return false;
    }

    public function getModule( $name ) {
        $name = ucfirst( $name );
        if ( isset( self::$installJson['modules'][$name] ) ) {
            if ( isset( self::$installJson['modules'][$name]['enabled'] ) ) {
                if ( self::$installJson['modules'][$name]['enabled'] == true ) {
                    self::$installJson['modules'][$name]['enabled_txt'] = '<span class="text-success">Yes</span>';
                } else {
                    self::$installJson['modules'][$name]['enabled_txt'] = '<span class="text-danger">No</span>';
                }
            }
            return self::$installJson['modules'][$name];
        }
        Debug::info( "install module not found: $name" );
        return false;
    }

    public function getModules( $includeObjects ) {
        if ( isset( self::$installJson['modules'] ) ) {
            if ( empty( $includeObjects ) ) {
                return self::$installJson['modules'];
            } else {
                $data = self::$installJson['modules'];
                foreach ( $data as $name => $module) {
                    $class_object = new $module['class'];
                    $data[$name]['class_object'] = $class_object;
                }
                return $data;
            }
        }
        Debug::error( "install modules not found" );
        return false;
    }

    public function getNode( $name) {
        if ( isset( self::$installJson[$name] ) ) {
            return self::$installJson[$name];
        }
    }

    public function setNode( $name, $value, $save = false ) {
        self::$installJson[$name] = $value;
        if ( $save !== false ) {
            return self::saveJson();
        }
        return true;
    }

    public function setModule( $name, $value, $save = false ) {
        if ( !isset( self::$installJson['modules'] ) ) {
            self::$installJson['modules'] = [];
        }
        self::$installJson['modules'][$name] = $value;
        if ( $save !== false ) {
            return self::saveJson();
        }
        return true;
    }

    private function removeModule( $name, $save = false ) {
        if ( !isset( self::$installJson['modules'] ) ) {
            Debug::error( 'No modules installed' );
            return false;
        }
        if ( !isset( self::$installJson['modules'][$name] ) ) {
            Debug::error( 'Module not installed' );
            return false;
        }
        self::$installJson['modules'][$name] = null;
        if ( $save !== false ) {
            return self::saveJson();
        }
        return true;
    }

    public function findModelFlags( $classObject ) {
        $install_flags = [];
        foreach ( self::MATRIX_MAP as $install_flag => $matrix_name ) {
            if ( !empty( $classObject->$matrix_name ) ) {
                $install_flags[$install_flag] = true;
            } else {
                $install_flags[$install_flag] = false;
            }
        }
        return $install_flags;
    }

    public function getPluginInfo( $folder) {
        $object = self::emptyModule( 'plugin', $folder );

        $location = $folder . 'plugin.php';
        if ( file_exists( $location ) ) {
            include_once $location;
        } else {
            self::$errors[] = ['errorInfo' => "Could not find the requested plugin file: $location"];
            return $object;
        }

        if ( ! class_exists( $object->class ) ) {
            Debug::warn( 'Cannot get plugin version from class: ' . $object->class . ', class does NOT exist.');
            return $object;
        }

        $class_object = new $object->class;
        $object->version = $class_object->pluginVersion;

        $module = $class_object->module;

        if ( false !== $module ) {
            $objectArray =  (array) $object;
            $object = (object) array_replace( $objectArray, $module );
        }
        $object->class_object = $class_object;

        return $object;
    }

    public function getModelInfo( $filename, $folder = '' ) {
        Debug::debug( 'getModelInfo filename: ' . $filename . ', folder: ' . $folder);
        $object = self::emptyModule( 'model', $folder, $filename );

        if ( ! class_exists( $object->class ) ) {
            Debug::warn( 'Cannot get model version from class: ' . $object->class . ', class does NOT exist.');
            return $object;
        }

        $class_object = new $object->class;
        $object->version = $class_object->modelVersion;

        $module = $this->getModule( $object->name );
        if ( false !== $module ) {
            $objectArray =  (array) $object;
            $object = (object) array_replace( $objectArray, $module );
        }
        $object->class_object = $class_object;

        return $object;
    }

    public function getAvailablePlugins() {
        $plugins = Plugin::getPluginDirectories();
        $list = [];
        foreach ( $plugins as $pluginName => $locations ) {
            foreach ( $locations as $location ) {
                foreach ( $location as $currentFolder => $file ) {
                    if ( 'plugin.php' == $file ) {
                        $list[ $pluginName ] = $this->getPluginInfo( str_ireplace( 'plugin.php', '', $currentFolder )  );
                    }
                }
            }
        }
        return $list;
    }

    public function getModelList( $folder ) {
        $files = scandir( $folder );
        $models = [];

        array_shift( $files );
        array_shift( $files );

        foreach ( $files as $index => $filename ) {
            if ( stripos( $filename, '.php' ) ) {
                $list[] = $this->getModelInfo( $filename, $folder );
            }
        }
        return $list;
    }

    public function installPlugin( $module_data, $flags = [], $defaultFlagValue = true ) {
        Debug::log( 'Installing Plugin: ' . $module_data->name );
        $errors = [];

        if ( INSTALL_STATUS_INSTALLED === $module_data->installStatus  ) {
            Debug::warn( "$name has already been successfully installed" );
            return true;
        }

        if ( empty( $module_data->class_object ) ) {
            self::$errors[] = [ 'errorInfo' => 'installPlugin: class_object not found: ' . $module_data->class ];
            return false;
        }

        // normalize install flags
        foreach ( PLUGIN_INSTALL_FLAGS as $flag_type ) {
            // add any missing flags
            if ( ! isset( $flags[ $flag_type ] ) ) {
                $flags[ $flag_type ] = $defaultFlagValue;
            }

            // exclude any flags that have already been successfully installed
            if ( !empty( $module_data->$flag_type ) && $module_data->$flag_type == INSTALL_STATUS_SUCCESS ) {
                Debug::warn( "$flag_type has already been successfully executed" );
                $flags[ $flag_type ] = false;
            }
        }

        list( $install_data, $errors ) = $module_data->class_object->install( $flags );
        $objectArray =  (array) $module_data;
        $module_data = array_replace( $objectArray, $install_data );
        $module_data['installedVersion'] = $module_data['version'];
        $module_data['lastUpdate'] = time();

        if ( empty( $module_data['installDate'] ) ) {
            $module_data['installDate'] = time();
        }

        unset($module_data['class_object']); // we don't want the class object though
        $this->setModule( $module_data['name'], $module_data, true );
        $this->updateInstallStatus( $module_data['name'] );

        if ( !empty( $errors ) ) {
            self::$errors = array_merge( self::$errors, $errors );
            return false;
        }

        $errors[] = [ 'errorInfo' => $module_data['name'] . " Plugin has been installed." ];
        self::$errors = array_merge( self::$errors, $errors );
        return true;
    }

    public function uninstallPlugin( $module_data, $flags = [], $defaultFlagValue = true ) {
        Debug::log( 'Uninstalling Plugin: ' . $module_data->name );
        $errors = [];

        if ( empty( $module_data->class_object ) ) {
            self::$errors[] = [ 'errorInfo' => 'uninstallPlugin: class_object not found: ' . $module_data->class ];
            return false;
        }

        // normalize install flags
        foreach ( PLUGIN_INSTALL_FLAGS as $flag_type ) {
            // add any missing flags
            if ( ! isset( $flags[ $flag_type ] ) ) {
                $flags[ $flag_type ] = $defaultFlagValue;
            }
        }

        $errors = $module_data->class_object->uninstall( $flags );

        if ( !empty( $errors ) ) {
            self::$errors = array_merge( self::$errors, $errors );
            return false;
        }

        $this->removeModule( $module_data->name, true );
        $errors[] = [ 'errorInfo' => $module_data->name . " Plugin has been uninstalled." ];
        self::$errors = array_merge( self::$errors, $errors );
        return true;
    }

    public function installModel( $module_data, $flags = [], $defaultFlagValue = true, $updateModule = true ) {
        Debug::log( 'Installing Model: ' . $module_data->name );
        $errors = [];

        if ( INSTALL_STATUS_INSTALLED === $module_data->installStatus  ) {
            Debug::warn( "$module_data->name has already been successfully installed" );
            return true;
        }

        if ( empty( $module_data->class_object ) ) {
            self::$errors[] = [ 'errorInfo' => 'installModel class_object not found: ' . $module_data->class ];
            return false;
        }

        // normalize install flags
        $model_flags = $this->findModelFlags( $module_data->class_object );
        foreach ( MODEL_INSTALL_FLAGS as $flag_type ) {
            // add any missing flags
            if ( ! isset( $flags[ $flag_type ] ) ) {
                $flags[ $flag_type ] = $defaultFlagValue;
            }

            // exclude any flags that have already been successfully installed
            if ( !empty( $module_data->$flag_type ) && $module_data->$flag_type == INSTALL_STATUS_SUCCESS ) {
                Debug::warn( "$flag_type has already been successfully installed" );
                $flags[ $flag_type ] = false;
            }

            if ( $flags[ $flag_type ] === true ) {
                // exclude any flags we can't do anything with
                if ( ! isset( $model_flags[ $flag_type ] ) ) {
                    Debug::warn( "$flag_type cannot be installed due to installFlags on the model." );
                    $flags[ $flag_type ] = false;
                }

                // check to make sure we have the proper mapping
                $matrix = self::MATRIX_MAP[ $flag_type ];

                // exclude any flags we don't have a matric map for
                if ( empty( $module_data->class_object->$matrix ) ) {
                    Debug::warn( "$flag_type does not have a proper matrix map and cannot be installed." );
                    $module_data->$flag_type = INSTALL_STATUS_NOT_FOUND;
                }
            }
        }

        list( $install_data, $errors ) = $module_data->class_object->install( $flags );
        $objectArray =  (array) $module_data;
        $module_data = array_replace( $objectArray, $install_data );
        $module_data['installedVersion'] = $module_data['version'];
        $module_data['lastUpdate'] = time();

        if ( empty( $module_data['installDate'] ) ) {
            $module_data['installDate'] = time();
        }

        if ($updateModule) {
            unset($module_data['class_object']); // we don't want the class object though
            $this->setModule( $module_data['name'], $module_data, true );
            $this->updateInstallStatus( $module_data['name'] );
        }

        if ( !empty( $errors ) ) {
            self::$errors = array_merge( self::$errors, $errors );
            return false;
        }

        $errors[] = [ 'errorInfo' => $module_data['name'] . " model has been installed." ];
        self::$errors = array_merge( self::$errors, $errors );
        return true;
    }

    public function uninstallModel( $module_data, $flags = [], $defaultFlagValue = true, $updateModule = true ) {
        Debug::log( 'Uninstalling Model: ' . $module_data->name );
        $errors = [];

        if ( empty( $module_data->class_object ) ) {
            self::$errors[] = [ 'errorInfo' => 'uninstallModel: class_object not found: ' . $module_data->class ];
            return false;
        }

        // normalize install flags
        $model_flags = $this->findModelFlags( $module_data->class_object );
        foreach ( MODEL_INSTALL_FLAGS as $flag_type ) {
            // add any missing flags
            if ( ! isset( $flags[ $flag_type ] ) ) {
                $flags[ $flag_type ] = $defaultFlagValue;
            }

            if ( $flags[ $flag_type ] === true ) {
                // exclude any flags we can't do anything with
                if ( ! isset( $model_flags[ $flag_type ] ) ) {
                    Debug::warn( "$flag_type cannot be installed due to installFlags on the model." );
                    $flags[ $flag_type ] = false;
                }

                // check to make sure we have the proper mapping
                $matrix = self::MATRIX_MAP[ $flag_type ];

                // exclude any flags we don't have a matric map for
                if ( empty( $module_data->class_object->$matrix ) ) {
                    Debug::warn( "$flag_type does not have a proper matrix map and cannot be uninstalled." );
                    $module_data->$flag_type = INSTALL_STATUS_NOT_FOUND;
                }
            }
        }

        list( $install_data, $errors ) = $module_data->class_object->uninstall( $flags );

        if ( !empty( $errors ) ) {
            self::$errors = array_merge( self::$errors, $errors );
            return false;
        }

        $errors[] = [ 'errorInfo' => $module_data->name . " model has been uninstalled." ];
        self::$errors = array_merge( self::$errors, $errors );
        return true;
    }

    private function updateInstallStatus( $name ) {
        $modelInfo = $this->getModule( $name );
        if ( $modelInfo === false ) {
            return;
        }
        if ( $modelInfo['type'] == 'plugin' ) {
            $flags = PLUGIN_INSTALL_FLAGS;
        } else {
            $flags = MODEL_INSTALL_FLAGS;
        }

        foreach ( $flags as $flag_type ) {
            if ( ! in_array( $modelInfo[ $flag_type ], [ INSTALL_STATUS_SUCCESS, INSTALL_STATUS_NOT_REQUIRED ] ) ) {
                $modelInfo['installStatus'] = INSTALL_STATUS_PARTIALLY_INSTALLED;
                break;
            }
            $modelInfo['installStatus'] = INSTALL_STATUS_INSTALLED;
        }
        $this->setModule( $name, $modelInfo, true );
    }

    /**
     * Checks the root directory for a .htaccess file and compares it with
     * the .htaccess file the application generates by default.
     *
     * NOTE: The $override flag will cause this function to automatically generate a
     * new htaccess file if the .htaccess found in the root directory does not match
     * the default generated version.
     *
     * @param  boolean $create - Optional flag to generate and save a new htaccess
     *                           if none is found.
     *
     * @return boolean - Returns true if the htaccess file was found or
     *                   created, false otherwise.
     */
    public function saveHtaccess( $text = false ) {
        if ( false === $text ) {
            $text = $this->generateHtaccess();
        }
        if ( file_exists( HTACCESS_LOCATION ) ) {
            Debug::error( "Can't overwrite existing htaccess file" );
            return false;
        }
        return file_put_contents( HTACCESS_LOCATION, $text );
    }

    public function checkHtaccess( $create = false ) {
        $check = 0;
        $findRewrite1 = "RewriteEngine On\n";
        $findRewrite2 = 'RewriteBase ' . Routes::getRoot() . "\n\n";
        if ( file_exists( HTACCESS_LOCATION ) ) {
            $htaccess = file_get_contents( HTACCESS_LOCATION );
            if ( $htaccess === $this->generateHtaccess() ) {
                return true;
            }
            if ( stripos( $htaccess, $findRewrite1 ) ) {
                $check++;
            }
            if ( stripos( $htaccess, $findRewrite2 ) ) {
                $check++;
            }
            if ( $check === 2 ) {
                return true;
            }
        }
        return false;
    }

    public function baseHtaccess() {
        $out = "# Intercepts for images not found
            RewriteCond %{REQUEST_FILENAME} !-f
            RewriteCond %{REQUEST_FILENAME} !-d
            RewriteRule ^images/(.*)$ index.php?error=image404&url=$1 [L,NC,QSA]

            # Intercepts for uploads not found
            RewriteCond %{REQUEST_FILENAME} !-f
            RewriteCond %{REQUEST_FILENAME} !-d
            RewriteRule ^uploads/(.*)$ index.php?error=upload404&url=$1 [L,NC,QSA]

            # Intercepts other errors
            RewriteRule ^errors/(.*)$ index.php?error=$1 [L,NC,QSA]

            # Intercept all traffic not originating locally and not going to images or uploads
            RewriteCond %{REMOTE_ADDR} !^127\.0\.0\.1
            RewriteCond %{REMOTE_ADDR} !^\:\:1
            RewriteCond %{REQUEST_URI} !^(.*)/images/(.*)$ [NC]
            RewriteCond %{REQUEST_URI} !^(.*)/uploads/(.*)$ [NC]
            RewriteCond %{REQUEST_URI} !^(.*).js$ [NC]
            RewriteCond %{REQUEST_URI} !^(.*).css$ [NC]
            RewriteCond %{REQUEST_URI} !^(.*).ico$ [NC]
            RewriteRule ^(.+)$ index.php?url=$1 [QSA,L]

            # Catchall for any non existent files or folders
            RewriteCond %{REQUEST_FILENAME} !-f
            RewriteCond %{REQUEST_FILENAME} !-d
            RewriteRule ^(.+)$ index.php?url=$1 [QSA,L]";
        return str_ireplace( '            ', '', $out );
    }

    /**
     * Generates the default htaccess file for the application. This will funnel
     * all traffic that comes into the application directory to index.php where we
     * use that data to construct the desired page using the controller.
     *
     * @param  string $docroot - A custom document root to use instead of the default.
     *
     * @return string   - The generated contents of the htaccess file.
     */
    protected function generateHtaccess( $docroot = null, $rewrite = true ) {
        $out = '';
        if ( empty( $docroot ) ) {
            $docroot = Routes::getRoot();
        }
        if ( $rewrite === true ) {
            $out .= "RewriteEngine On\n\n";
        }
        $out .= "RewriteBase $docroot\n\n";
        $out .= $this->baseHtaccess();
        return $out;
    }
}
