#!/usr/bin/env php
<?php

use Laminas\Config\Writer\PhpArray;
use Laminas\ApiTools\Configuration\ConfigResource;

// Setup/verify autoloading
if (file_exists($a = __DIR__ . '/../../../autoload.php')) {
    require $a;
} elseif (file_exists($a = __DIR__ . '/../vendor/autoload.php')) {
    require $a;
} else {
    fwrite(STDERR, 'Cannot locate autoloader; please run "composer install"' . PHP_EOL);
    exit(1);
}

// Configuration
$path = realpath(getcwd());
$composer = 'composer';
$modulesToRemove = [
    'AssetManager',
    'Laminas\ApiTools\Provider',
    'Laminas\DevelopmentMode',
];
$modulesToAdd = [
    'Laminas\Cache',
    'Laminas\Db',
    'Laminas\Filter',
    'Laminas\Hydrator',
    'Laminas\InputFilter',
    'Laminas\I18n',
    'Laminas\Log',
    'Laminas\Mvc\I18n',
    'Laminas\Paginator',
    'Laminas\Router',
    'Laminas\Validator',
];
$devModulesToRemove = [
    'ZFTool',
];
$devModulesToAdd = [];

// Check arguments
if ($argc > 1) {
    if (in_array($argv[1], ['-h', '--help'], true)) {
        help();
        exit(0);
    }

    if ($argc != 3 || ! in_array($argv[1], ['-c', '--composer'], true)) {
        fwrite(STDERR, "Invalid arguments supplied\n\n");
        help(STDERR);
        exit(1);
    }

    if (! file_exists($argv[2])) {
        fwrite(STDERR, "Provided composer binary does not exist\n\n");
        help(STDERR);
        exit(1);
    }

    $composer = $argv[2];
}

// Workflow
echo "Upgrading your Laminas API Tools application to use Laminas v3 components.\n\n";

echo "Updating composer.json...\n";
$result = updateComposerJson($path);
if (! $result->status) {
    fwrite(STDERR, sprintf("%s\n", $result->message));
    exit(1);
}

echo "Updating module list...\n";
$result = updateModulesList($path, $modulesToRemove, $modulesToAdd);
if (! $result->status) {
    fwrite(STDERR, sprintf("%s\n", $result->message));
    exit(1);
}

echo "Updating development module list...\n";
foreach (['development.config.php', 'development.config.php.dist'] as $configFile) {
    if (! file_exists(sprintf('%s/config/%s', $path, $configFile))) {
        continue;
    }
    $result = updateDevelopmentConfig($configFile, $path, $devModulesToRemove, $devModulesToAdd);
    if (! $result->status) {
        fwrite(STDERR, sprintf("%s\n", $result->message));
        exit(1);
    }
}

echo "Removing previously installed dependencies...\n";
removeInstalledDependencies($path);

echo "Installing new dependencies...\n";
$result = installDependencies($composer);
if (! $result->status) {
    fwrite(STDERR, sprintf("%s\n", $result->message));
    exit(1);
}

echo "Updating public/index.php for legacy development mode tooling...\n";
$result = updateIndexFile($path);
if (! $result->status) {
    fwrite(STDERR, sprintf("%s\n", $result->message));
    exit(1);
}

echo "\nUpgrade complete!\n";
exit(0);

// Functions

/**
 * @param string $path
 * @return Result
 */
function updateComposerJson($path)
{
    $composerJsonFile = sprintf('%s/composer.json', $path);

    if (! file_exists($composerJsonFile)) {
        return new Result(
            false,
            'Could not locate composer.json; are you running this from your project root?'
        );
    }

    $composerJson = file_get_contents($path . '/composer.json');
    $composer = json_decode($composerJson, true);

    // Remove entries
    if (isset($composer['require']['zendframework/zendframework'])) {
        unset($composer['require']['zendframework/zendframework']);
    }
    if (isset($composer['require']['rwoverdijk/assetmanager'])) {
        unset($composer['require']['rwoverdijk/assetmanager']);
    }
    if (isset($composer['require-dev']['zendframework/zftool'])) {
        unset($composer['require-dev']['zendframework/zftool']);
    }

    // Add entries
    $composer['require']['laminas/laminas-cache'] = '^2.7.1';
    $composer['require']['laminas/laminas-mvc-i18n'] = '^1.0';
    $composer['require']['laminas/laminas-log'] = '^2.9';
    $composer['require-dev']['laminas-api-tools/api-tools-asset-manager'] = '^1.0';

    // Update existing entries
    $composer['require']['laminas/laminas-development-mode'] = '^3.0';
    $composer['require-dev']['laminas/laminas-developer-tools'] = '^1.1';

    $composerJson = json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    file_put_contents($composerJsonFile, $composerJson . "\n");

    return new Result(true);
}

/**
 * @param string $path
 * @param array $modulesToRemove
 * @param array $modulesToAdd
 * @return Result
 */
function updateModulesList($path, array $modulesToRemove, array $modulesToAdd)
{
    $moduleListFile = sprintf('%s/config/modules.config.php', $path);

    if (! file_exists($moduleListFile)) {
        return new Result(
            false,
            'Could not locate config/modules.config.php; are you running this from an Laminas API Tools project?'
        );
    }

    $moduleList = include $moduleListFile;

    if (! is_array($moduleList)) {
        return new Result(
            false,
            'config/modules.config.php did not return an array; please check the file.'
        );
    }

    // Create a backup
    copy($moduleListFile, $moduleListFile . '.old');

    $configWriter = new PhpArray();
    $configWriter->setUseBracketArraySyntax(true);
    $configWriter->setUseClassNameScalars(true);

    $configResource = new ConfigResource(
        $moduleList,
        $moduleListFile,
        $configWriter
    );

    // Remove modules
    $modules = array_filter($moduleList, function ($module) use ($modulesToRemove) {
        return ! in_array($module, $modulesToRemove);
    });

    // Add modules
    array_splice($modules, 0, 0, $modulesToAdd);

    // Write config file
    $configResource->overWrite($modules);
    postProcessConfigFile($moduleListFile);

    return new Result(true);
}

/**
 * @param string $file
 * @param string $path
 * @param array $modulesToRemove
 * @param array $modulesToAdd
 * @return Result
 */
function updateDevelopmentConfig($file, $path, array $modulesToRemove, array $modulesToAdd)
{
    $moduleListFile = sprintf('%s/config/%s', $path, $file);

    if (! file_exists($moduleListFile)) {
        return new Result(
            false,
            sprintf('Could not locate config/%s; are you running this from an Laminas API Tools project?', $file)
        );
    }

    $moduleList = include $moduleListFile;

    if (! is_array($moduleList)) {
        return new Result(
            false,
            sprintf('config/%s did not return an array; please check the file.', $file)
        );
    }

    if (! is_array($moduleList['modules'])) {
        return new Result(
            false,
            sprintf('config/%s does not contain any modules; please check the file.', $file)
        );
    }

    // Create a backup
    copy($moduleListFile, $moduleListFile . '.old');

    $configWriter = new PhpArray();
    $configWriter->setUseBracketArraySyntax(true);
    $configWriter->setUseClassNameScalars(true);

    $configResource = new ConfigResource(
        $moduleList,
        $moduleListFile,
        $configWriter
    );

    // Remove modules
    $modules = array_filter($moduleList['modules'], function ($module) use ($modulesToRemove) {
        return ! in_array($module, $modulesToRemove);
    });

    // Add modules
    array_splice($modules, 0, 0, $modulesToAdd);

    // Write config file
    $configResource->patchKey('modules', $modules);
    postProcessConfigFile($moduleListFile);

    return new Result(true);
}

/**
 * @param string $filename
 * @return void
 */
function postProcessConfigFile($filename)
{
    $contents = file_get_contents($filename);

    // Add comment to top of file
    $template =<<<'EOC'
<?php

/**
 * File generated by vendor/bin/api-tools-upgrade-to-1.5.
 *
 * Original contents are available in config/%s.old
 */

EOC;
    $template = sprintf($template, basename($filename));
    $contents = preg_replace('/^\<\?php/', $template, $contents);

    // Remove integer array indices
    $contents = preg_replace('/^(\s+)\d+\s+\=\> /m', '$1', $contents);

    file_put_contents($filename, $contents);
}

/**
 * @param string $path
 * @return void
 */
function removeInstalledDependencies($path)
{
    $lockPath = sprintf('%s/composer.lock', $path);
    if (file_exists($lockPath)) {
        unlink($lockPath);
    }

    removePath(sprintf('%s/vendor', $path));
}

/**
 * @param string $path Directory to remove
 * @return void
 */
function removePath($path)
{
    if (! is_dir($path)) {
        return;
    }

    foreach (new DirectoryIterator($path) as $file) {
        if (in_array($file->getBaseName(), ['.', '..'], true)) {
            continue;
        }

        if ($file->isDir() && ! $file->isLink()) {
            removePath($file->getPathname());
            continue;
        }

        unlink($file->getPathname());
    }

    rmdir($path);
}

/**
 * @var string $composer Composer binary
 * @return Result
 */
function installDependencies($composer)
{
    $status = 0;
    $command = sprintf('%s install', $composer);
    system($command, $status);

    if ($status !== 0) {
        return new Result(
            false,
            'composer install did not complete successfully; run manually to debug'
        );
    }

    return new Result(true);
}

/**
 * @var string $path
 * @return Result
 */
function updateIndexFile($path)
{
    $indexPath = sprintf('%s/public/index.php', $path);

    if (! file_exists($indexPath)) {
        return new Result(
            false,
            'Could not locate public/index.php; are you running this from an Laminas API Tools project?'
        );
    }

    $indexFile = file_get_contents($indexPath);

    $template =<<<'EOT'


// Redirect legacy requests to enable/disable development mode to new tool
if (php_sapi_name() === 'cli'
    && $argc > 2
    && 'development' == $argv[1]
    && in_array($argv[2], ['disable', 'enable'])
) {
    // Windows needs to execute the batch scripts that Composer generates,
    // and not the Unix shell version.
    $script = defined('PHP_WINDOWS_VERSION_BUILD') && constant('PHP_WINDOWS_VERSION_BUILD')
        ? '.\\vendor\\bin\\laminas-development-mode.bat'
        : './vendor/bin/laminas-development-mode';
    system(sprintf('%s %s', $script, $argv[2]), $return);
    exit($return);
}
EOT;
    $indexFile = preg_replace(
        '#(chdir\(dirname\(__DIR__\)\\);)#s',
        '$1' . $template,
        $indexFile
    );

    file_put_contents($indexPath, $indexFile);

    return new Result(true);
}

/**
 * Emit the help message on the specified stream.
 *
 * @var resource $stream
 * @return void
 */
function help($stream = STDOUT)
{
    $usage =<<<'EOC'
Update an Laminas API Tools component to use Laminas component v3 releases.

Usage:

  vendor/bin/api-tools-upgrade-to-1.5 [options]

Options:

  -h|--help               Display this message
  -c|--composer <path>    Path to composer binary; defaults to "composer"
                          (assumes discoverable via user PATH)

EOC;

    fwrite($stream, $usage);
}

/**
 * Value object describing an operation result.
 */
class Result
{
    /**
     * @var string
     */
    public $message = '';

    /**
     * @var bool
     */
    public $status;

    /**
     * @param bool $status
     * @param string $message
     */
    public function __construct($status, $message = '')
    {
        $this->status = $status;
        $this->message = $message;
    }
}
