#!/bin/php

# ------------------------------------------------------------------
# Nosial Code Compiler (NCC) Installation Script
#
#   Nosial Code Compiler is a program written in PHP designed
#   to be a multipurpose compiler, package manager and toolkit.
#
# Dependency:
#     PHP 8.0+
# ------------------------------------------------------------------

<?PHP

    use ncc\Enums\ConsoleColors;
    use ncc\ncc;
    use ncc\Objects\CliHelpSection;
    use ncc\ThirdParty\Symfony\Filesystem\Exception\IOException;
    use ncc\ThirdParty\Symfony\Filesystem\Filesystem;
    use ncc\ThirdParty\Symfony\process\PhpExecutableFinder;
    use ncc\ThirdParty\Symfony\Yaml\Yaml;
    use ncc\Utilities\Console;
    use ncc\Utilities\Functions;
    use ncc\Utilities\IO;
    use ncc\Utilities\PathFinder;
    use ncc\Utilities\Resolver;
    use ncc\Utilities\Validate;
    use ncc\Extensions\ZiProto\ZiProto;

    # Global Variables
    $NCC_INSTALL_PATH = DIRECTORY_SEPARATOR . 'usr' . DIRECTORY_SEPARATOR . 'share' . DIRECTORY_SEPARATOR . 'ncc';
    $NCC_DATA_PATH = DIRECTORY_SEPARATOR . 'var' . DIRECTORY_SEPARATOR . 'ncc';
    $NCC_CHECKSUM = __DIR__ . DIRECTORY_SEPARATOR . 'checksum.bin';
    $NCC_AUTOLOAD = __DIR__ . DIRECTORY_SEPARATOR . 'autoload.php';
    $NCC_PHP_EXECUTABLE = null;

    // Require NCC
    if(!file_exists($NCC_AUTOLOAD))
    {
        print(sprintf('Cannot find autoload file \'%s\', installation failed successfully.', $NCC_AUTOLOAD) . PHP_EOL);
        exit(1);
    }

    require($NCC_AUTOLOAD);

    try
    {
        // Initialize NCC
        define('NCC_CLI_MODE', 1);
        ncc::initialize();
    }
    catch (Exception $e)
    {
        Console::outException('Cannot initialize NCC, ' . $e->getMessage() . ' (Error Code: ' . $e->getCode() . ')', $e, 1);
        return;
    }

    $NCC_ARGS = null;
    $NCC_FILESYSTEM = new Filesystem();

    // Options Parser
    if(isset($argv))
    {
        $NCC_ARGS = Resolver::parseArguments(implode(' ', $argv));
    }

    $NCC_AUTO_MODE = ($NCC_ARGS !== null && isset($NCC_ARGS['auto']));
    $NCC_BYPASS_OS_CHECK = ($NCC_ARGS !== null && isset($NCC_ARGS['bypass-os-check']));
    $NCC_BYPASS_CHECKSUM = ($NCC_ARGS !== null && isset($NCC_ARGS['bypass-checksum']));

    if(isset($NCC_ARGS['help']))
    {
        $options = [
            new CliHelpSection(['--help'], 'Displays this help menu about the installer'),
            new CliHelpSection(['--bypass-os-check'], 'Bypasses the check for the current operating system'),
            new CliHelpSection(['--bypass-checksum'], 'Bypasses the checksum for the installation files'),
        ];

        $options_padding = Functions::detectParametersPadding($options) + 4;

        Console::out('Usage: ' . __FILE__ . ' [options]');
        Console::out('Options:' . PHP_EOL);
        foreach($options as $option)
        {
            Console::out('   ' . $option->toString($options_padding));
        }
        exit(0);
    }

    // Detect if running in Windows
    if(!$NCC_BYPASS_OS_CHECK && strtolower(PHP_OS_FAMILY) === 'windows')
    {
        Console::outError(sprintf('This installer is not compatible with Windows, detected OS: %s (This check can be bypassed with --bypass-os-check)', PHP_OS), true, 1);
        return;
    }

    if(!function_exists('posix_getuid'))
    {
        Console::outError('The function posix_getuid() is not available on your system, please make sure the extension `php-common` is installed', true, 1);
        return;
    }

    if (posix_getuid() !== 0)
    {
        Console::outError('You must be running as root', true, 1);
        return;
    }

    // Find the PHP executable
    $NCC_PHP_EXECUTABLE = (new PhpExecutableFinder())->find();

    if(!$NCC_PHP_EXECUTABLE)
    {
        Console::outError('Cannot find PHP executable path', true ,1);
        return;
    }

    // Check for the required files
    $required_files = [
        __DIR__ . DIRECTORY_SEPARATOR . 'LICENSE',
        __DIR__ . DIRECTORY_SEPARATOR . 'build_files',
        __DIR__ . DIRECTORY_SEPARATOR . 'ncc.sh',
        __DIR__ . DIRECTORY_SEPARATOR . 'default_config.yaml',
    ];
    foreach($required_files as $path)
    {
        if(!$NCC_FILESYSTEM->exists($path))
        {
            Console::outError('Missing file \'' . $path . '\', installation failed.', true, 1);
            exit(1);
        }
    }

    // Preform the checksum validation
    if(!$NCC_BYPASS_CHECKSUM)
    {
        if(!$NCC_FILESYSTEM->exists($NCC_CHECKSUM))
        {
            Console::outError(sprintf('The checksum file \'%s\' was not found, the checksum cannot be validated', $NCC_CHECKSUM), 1, true);
            return;
        }

        try
        {
            Console::out('Testing checksum...');
            $checksum = ZiProto::decode(IO::fread($NCC_CHECKSUM));
        }
        catch(Exception $e)
        {
            Console::outError($e->getMessage(), true, 1);
            return;
        }

        $checksum_failed = false;

        foreach($checksum as $path => $hash)
        {
            if(!file_exists(__DIR__ . DIRECTORY_SEPARATOR . $path))
            {
                Console::outError('Cannot check file, \'' . $path . '\' not found.');
                $checksum_failed = true;
            }
            elseif(hash_file('sha256', __DIR__ . DIRECTORY_SEPARATOR . $path) !== $hash)
            {
                Console::outWarning('The file \'' . $path . '\' does not match the original checksum');
                $checksum_failed = true;
            }
        }

        if($checksum_failed)
        {
            Console::outError('Checksum failed, the contents of the program cannot be verified to be safe', true, 1);
            return;
        }
    }

    // Check the installed extensions and report
    Console::out('Checking installed extensions...');
    $extensions = Validate::requiredExtensions();
    foreach($extensions as $ext => $installed)
    {
        if($installed)
        {
            Console::out("$ext ... " . Console::formatColor("installed", ConsoleColors::LIGHT_GREEN->value));
        }
        else
        {
            Console::out("$ext ... " . Console::formatColor("missing", ConsoleColors::LIGHT_RED->value));
        }
    }

    try
    {
        // Attempt to load version information
        $version_information = ncc::getVersionInformation();
    }
    catch (Exception $e)
    {
        Console::outException('Cannot get version information, ' . $e->getMessage() . ' (Error Code: ' . $e->getCode() . ')', $e, 1);
        return;
    }

    // Display version information
    Console::out('NCC Version: ' . NCC_VERSION_NUMBER . ' (' . NCC_VERSION_BRANCH . ')');
    Console::out('Build Flags: ' . implode(',', NCC_VERSION_FLAGS));

    foreach($version_information->getComponents() as $component)
    {
        $full_name = $component->getVendor() . '/' . $component->getPackageName();

        try
        {
            Console::out(Console::formatColor($full_name, ConsoleColors::GREEN->value) . ' Version: ' . Console::formatColor($component->getVersion(), ConsoleColors::LIGHT_MAGENTA->value));
        }
        catch (Exception $e)
        {
            Console::outWarning('Cannot determine component version of ' . Console::formatColor($full_name, ConsoleColors::GREEN->value));
        }
    }

    if(!$NCC_AUTO_MODE && !Console::getBooleanInput('Do you want install NCC?')) 
    {
        Console::outError('Installation cancelled by user', true, 1);
        return;
    }
    
    Console::out('Starting installation');
    
    // Prepare installation
    if($NCC_FILESYSTEM->exists($NCC_INSTALL_PATH))
    {
        try
        {
            $NCC_FILESYSTEM->remove([$NCC_INSTALL_PATH]);
        }
        catch(IOException $e)
        {
            Console::outException('Cannot delete directory \'' . $NCC_INSTALL_PATH . '\', ' . $e->getMessage(), $e, 1);
            return;
        }
    }

    $NCC_FILESYSTEM->mkdir($NCC_INSTALL_PATH, 0755);

    // Copy files to the installation path
    try
    {
        Console::out('Copying files to \'' . $NCC_INSTALL_PATH . '\'');
        $build_files = explode("\n", IO::fread(__DIR__ . DIRECTORY_SEPARATOR . 'build_files'));
    }
    catch(Exception $e)
    {
        Console::outError($e->getMessage(), true, 1);
        return;
    }
    
    $total_items = count($build_files);
    //$processed_items = 1;
    $progress_bar = new \ncc\Utilities\ConsoleProgressBar('Installing ncc', $total_items);

    foreach ($build_files as $item)
    {
        $progress_bar->setMiscText($item, true);
        $source = __DIR__ . DIRECTORY_SEPARATOR . $item;
        $destination = $NCC_INSTALL_PATH . DIRECTORY_SEPARATOR . $item;

        if (is_file($source))
        {
            $parent_directory = dirname($destination);

            if(!is_dir($parent_directory))
            {
                $NCC_FILESYSTEM->mkdir($parent_directory, 0755);
            }

            $NCC_FILESYSTEM->copy($source, $destination);
            $NCC_FILESYSTEM->chmod([$destination], 0755);

            if (!$NCC_FILESYSTEM->exists($destination))
            {
                Console::outError(sprintf('Failed to copy file \'%s\' to \'%s\'', $source, $destination), true, 1);
                return;
            }
        }

        //++$processed_items;
        //Console::inlineProgressBar($processed_items, $total_items);
        $progress_bar->increaseValue(1, true);
    }

    $progress_bar->setMiscText('done', true);
    unset($progress_bar);

    // Initialize ncc's files
    try
    {
        if(is_file(__DIR__ . DIRECTORY_SEPARATOR . 'default_repositories.json'))
        {
            Functions::initializeFiles($NCC_INSTALL_PATH, Functions::loadJsonFile(__DIR__ . DIRECTORY_SEPARATOR . 'default_repositories.json', Functions::FORCE_ARRAY));
        }
        else
        {
            Functions::initializeFiles($NCC_INSTALL_PATH);
        }
    }
    catch(Exception $e)
    {
        Console::outException('Cannot initialize NCC files, ' . $e->getMessage(), $e, 1);
        return;
    }


    // Generate executable shortcut
    try
    {
        $executable_shortcut = IO::fread(__DIR__ . DIRECTORY_SEPARATOR . 'ncc.sh');
    }
    catch(Exception $e)
    {
        Console::outException(sprintf('Failed to read file \'%s\', %s', __DIR__ . DIRECTORY_SEPARATOR . 'ncc.sh', $e->getMessage()), $e, 1);
        return;
    }

    $executable_shortcut = str_ireplace(['%php_exec', '%ncc_exec'], [$NCC_PHP_EXECUTABLE, $NCC_INSTALL_PATH . DIRECTORY_SEPARATOR . 'ncc'],
        $executable_shortcut
    );

    $bin_paths = [
        DIRECTORY_SEPARATOR . 'usr' . DIRECTORY_SEPARATOR . 'local' . DIRECTORY_SEPARATOR . 'bin',
        DIRECTORY_SEPARATOR . 'usr' . DIRECTORY_SEPARATOR . 'bin',
    ];

    foreach($bin_paths as $path)
    {
        $path .= DIRECTORY_SEPARATOR . 'ncc';

        // Delete old versions of the executable shortcuts.
        if($NCC_FILESYSTEM->exists($path))
        {
            $NCC_FILESYSTEM->remove($path);
        }

        try
        {
            IO::fwrite($path, $executable_shortcut);
            $NCC_FILESYSTEM->chmod([$path], 0755);
        }
        catch (Exception $e)
        {
            Console::outException(sprintf('Failed to write file \'%s\', %s', $path, $e->getMessage()), $e, 1);
            return;
        }
    }

    // Backup the configuration file
    $config_backup = null;

    try
    {
        if ($NCC_FILESYSTEM->exists(PathFinder::getConfigurationFile()))
        {
            $config_backup = Yaml::parseFile(PathFinder::getConfigurationFile());
        }
    }
    catch (Exception $e)
    {
        Console::outError($e->getMessage(), true, 1);
        return;
    }

    // Create/Update configuration file
    $config_obj = Yaml::parseFile(__DIR__ . DIRECTORY_SEPARATOR . 'default_config.yaml');

    if(!function_exists('array_replace_recursive'))
    {
        /**
         * @param $array
         * @param $array1
         * @return array|mixed
         * @author <msahagian@dotink.org>
         * @noinspection PhpMissingReturnTypeInspection
         */
        function array_replace_recursive($array, $array1)
        {
            // handle the arguments, merge one by one
            $args = func_get_args();

            if (!is_array($args[0]))
            {
                return $args[0];
            }

            for ($i = 1, $i_max = count($args); $i < $i_max; $i++)
            {
                if (is_array($args[$i]))
                {
                    $array = recurse($array, $args[$i]);
                }
            }

            return $array;
        }
    }

    if(!function_exists('recurse'))
    {
        /**
         * @param $array
         * @param $array1
         * @return mixed
         * @author <msahagian@dotink.org>
         * @noinspection PhpMissingReturnTypeInspection
         */
        function recurse($array, $array1)
        {
            foreach ($array1 as $key => $value)
            {
                // create new key in $array if it is empty or not an array
                /** @noinspection PhpConditionAlreadyCheckedInspection */
                if (!isset($array[$key]) || (isset($array[$key]) && !is_array($array[$key])))
                {
                    $array[$key] = array();
                }

                // overwrite the value in the base array
                if (is_array($value))
                {
                    $value = recurse($array[$key], $value);
                }

                $array[$key] = $value;
            }
            return $array;
        }
    }


    if($config_backup !== null)
    {
        $config_obj = array_replace_recursive($config_obj, $config_backup);
    }

    if($config_backup === null)
    {
        Console::out('Generating ncc.yaml');
    }
    else
    {
        Console::out('Updating ncc.yaml');
    }

    try
    {
        IO::fwrite(PathFinder::getConfigurationFile(), Yaml::dump($config_obj), 0755);
    }
        catch (Exception $e)
    {
        Console::outException($e->getMessage(), $e, 1);
        return;
    }

    Console::out('NCC version: ' . NCC_VERSION_NUMBER . ' has been successfully installed');
    Console::out('For licensing information see \'' . $NCC_INSTALL_PATH . DIRECTORY_SEPARATOR . 'LICENSE\' or run \'ncc help --license\'');

    exit(0);