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

/**
 * storyplayer
 *         CLI tool for playing individual stories
 *
 *         Very handy for testing stories as you write them
 *
 * @author Stuart Herbert <stuart.herbert@datasift.com>
 * @copyright (c) 2012-present MediaSift Ltd.
 */

use Phix_Project\Autoloader4\PSR0_Autoloader;
use Phix_Project\Autoloader4\Autoloader_Path;

use Phix_Project\CliEngine;
use Phix_Project\CliEngine\Switches\LongHelpSwitch;
use Phix_Project\CliEngine\Switches\ShortHelpSwitch;
use Phix_Project\CliEngine\Switches\VersionSwitch;
use Phix_Project\CliEngine\Commands\HelpCommand;

use DataSift\Storyplayer\Injectables;

use DataSift\Storyplayer\Cli\BuildTestEnvironment_Command;
use DataSift\Storyplayer\Cli\CreateStory_Command;
use DataSift\Storyplayer\Cli\DeviceListHelper;
use DataSift\Storyplayer\Cli\ExtraConfigHelper;
use DataSift\Storyplayer\Cli\Install_Command;
use DataSift\Storyplayer\Cli\KillProcesses_Switch;
use DataSift\Storyplayer\Cli\ListHostsTable_Command;
use DataSift\Storyplayer\Cli\ListProcesses_Switch;
use DataSift\Storyplayer\Cli\ListSystems_Switch;
use DataSift\Storyplayer\Cli\ListTargets_Switch;
use DataSift\Storyplayer\Cli\PlayStory_Command;
use DataSift\Storyplayer\Cli\RuntimeConfigManager;
use DataSift\Storyplayer\Cli\Script_Command;
use DataSift\Storyplayer\Cli\ShowTestEnvironment_Command;
use DataSift\Storyplayer\Cli\StaticConfigManager;

use DataSift\Storyplayer\ConfigLib\DevicesList;
use DataSift\Storyplayer\DeviceLib\HardCodedDevices;
use DataSift\Storyplayer\ConfigLib\SystemsUnderTestList;
use DataSift\Storyplayer\SystemsUnderTestLib\HardCodedSystemsUnderTest;
use DataSift\Storyplayer\ConfigLib\TestEnvironmentsList;
use DataSift\Storyplayer\TestEnvironmentsLib\HardCodedTestEnvironments;

use DataSift\Storyplayer\ConfigLib\E4xx_TestEnvironmentFileMustReturnADefinition;
use DataSift\Storyplayer\DefinitionLib\E4xx_BadTestEnvironmentDefinition;

use DataSift\Stone\ConfigLib\E5xx_ConfigFileNotFound;
use DataSift\Stone\ConfigLib\E5xx_InvalidConfigFile;

use Stuart\MyVersion;

// where are we, and our useful utilities?
define('APP_BINDIR', __DIR__);

// enable signal handling
declare(ticks=1);

// we want all the memory
ini_set('memory_limit', -1);

/**
 * can we safely load this file?
 *
 * WE HAVE A PROBLEM WHEN RUNNING AS A PHAR ...
 *
 *
 * however, if you:
 *
 *  1) create a git clone of Storyplayer,
 *  2) build the PHAR file, AND
 *  3) run the PHAR file from the top level of the git clone without
 *     running 'composer install|update', then the following happens:
 *
 * our PHAR contains 'vendor/autoload.php' (auto-generated by Composer)
 * on disk we have an IDENTICAL 'vendor/autoload.php' file
 *
 * Storyplayer will load them both, causing a PHP fatal error
 *
 * WE CANNOT DISABLE LOADING vendor/autoload.php FROM DISK, because
 * Storyplayer has no idea whether that is its own autoload file, or
 * the autoload file of the project we want to test
 *
 * We're therefore forced to vet the Composer autoloader file that we try
 * to load, to make sure that we have not already loaded it
 */
function can_load($filename)
{
	static $filesSeen = array();

	// checksum the file
	$sum = md5_file($filename);

	// have we seen this file before?
	if (isset($filesSeen[$sum])) {
		// do nothing - already loaded
		return false;
	}

	// if we get here, we have a new file
	$filesSeen[$sum] = $sum;

	// we can load it
	return true;
}

// work out where the local source code might be
$localSrcDir = '';
$localSrcDirs = array(
	__DIR__ . '/../php',
	__DIR__ . '/../src/php',
	getcwd() . '/src/php',
	getcwd() . '/php'
);
foreach ($localSrcDirs as $key => $candidateDir) {
	if (!is_dir($candidateDir)) {
		unset($localSrcDirs[$key]);
	}
}

// work out where your local Storyplayer extensions might be
$localExtensions = [
	getcwd() . '/storyplayer/php',
];
foreach ($localExtensions as $key => $candidateDir) {
	if (!is_dir($candidateDir)) {
		unset($localExtensions[$key]);
	}
}

// do we have a composer autoloader?
$composerAutoloader = false;
$composerFiles = array (
	dirname($argv[0]) . "/../autoload.php",
	getcwd() . '/vendor/autoload.php',
	__DIR__ . '/../../vendor/autoload.php',
	__DIR__ . "/../../../../autoload.php",
);
foreach ($composerFiles as $composerFile) {
	if (file_exists($composerFile) && can_load($composerFile) &&!$composerAutoloader) {
		require_once($composerFile);
		$composerAutoloader = true;
	}
}

// do we need to explicitly load Phix's autoloader instead?
if (!$composerAutoloader) {
	// work out where the vendor folder might be
	$vendorDir = '';
	$vendorDirs = array(
		__DIR__ . '/../../../../',
		__DIR__ . '/../../',
		getcwd() . '/vendor/'
	);
	foreach ($vendorDirs as $key => $candidateDir) {
		if (is_dir($candidateDir)) {
			$vendorDir = $candidateDir;
		}
	}

	// load the autoloader file
	if (is_dir($vendorDir)) {
		require $vendorDir . '/Phix_Project/Autoloader4/PSR0/Autoloader.php';
	}
	else {
		require 'Phix_Project/Autoloader4/PSR0/Autoloader.php';
	}
}
else {
	$vendorDirs   = array();
}

// start autoloading
PSR0_Autoloader::startAutoloading();

// if we have a vendor folder, load from there
foreach ($vendorDirs as $vendorDir){
	Autoloader_Path::searchFirst($vendorDir);
}

// if we have a source dir, we also want to load from there
foreach ($localSrcDirs as $localSrcDir){
	Autoloader_Path::searchFirst($localSrcDir);
}

// if there are local storyplayer extensions, we also want to load from there
foreach ($localExtensions as $localExtensionDir) {
	Autoloader_Path::searchFirst($localExtensionDir);
}

// load our list of helper functions
require __DIR__.'/../php/DataSift/Storyplayer/functions.php';

// are there any local functions to load?
$localFunctions = "storyplayer/php/functions.php";
if (file_exists($localFunctions)) {
	// require_once() in case it has been loaded via Composer first
	require_once($localFunctions);
}

/**
 * it all happens here, baby
 *
 * @return integer status code to return to the shell
 */
function main($argv)
{
	// ====================================================================
	//
	// BOOTSTRAP THE APP
	//
	// --------------------------------------------------------------------

	// create our DI container
	$injectables = new Injectables();

	// activate support for printing to the user
	$output = $injectables->initOutputSupport();

	// activate support for parsing code
	$injectables->initCodeParserSupport();

	// activate support for printing data
	$injectables->initDataFormatterSupport();

	// create our built-in default config
	$defaultConfig = $injectables->initDefaultConfigSupport();

	try {
		// find our storyplayer config file
		$injectables->initStoryplayerConfigFilenameSupport();

		// load our storyplayer config
		$staticConfigManager = $injectables->initStaticConfigManagerSupport();
		$injectables->initStoryplayerConfigSupport($injectables, $injectables->storyplayerConfigFilename);

		// create our runtime config folder, and load any pre-existing
		// runtime config
		$runtimeConfigManager = $injectables->initRuntimeConfigSupport($injectables);
		$runtimeConfig        = $injectables->getRuntimeConfig();

		// a list of the additional files that the user can select from
		$sutList = new SystemsUnderTestList();
		$sutList->addHardCodedList(new HardCodedSystemsUnderTest);
		$sutList->findConfigs();

		$envList = new TestEnvironmentsList();
		$envList->addHardCodedList(new HardCodedTestEnvironments);
		$envList->findConfigs();

		$deviceList = new DevicesList();
		$deviceList->addHardCodedList(new HardCodedDevices);
		$deviceList->findConfigs();

		// put all of that into something we can give to the CliEngine
		$injectables->initKnownDevicesSupport($deviceList);
		$injectables->initKnownTestEnvironmentsSupport($envList);
		$injectables->initDefaultTestEnvironmentName($injectables);
		$injectables->initKnownSystemsUnderTestSupport($sutList);
		$injectables->initDefaultSystemUnderTestName($injectables);

		// do we have any command-line defaults set in the config?
		$injectables->initDefaultCommandLineSupport($injectables->storyplayerConfig, $injectables);
	}
	catch (E4xx_TestEnvironmentFileMustReturnADefinition $e)
	{
		// this happens when a PHP-style test environment file either does
		// not return anything, or does not return the expected object
		$output->logCliError($e->getMessage());
		exit(1);
	}
	catch (E4xx_BadTestEnvironmentDefinition $e)
	{
		// this happens when a PHP-style test environment file passes an
		// invalid input into one of the DefinitionLib classes
		$output->logCliError($e->getMessage());
		exit(1);
	}
	catch (Exception $e)
	{
		// if this happens, we need a full stack trace to understand
		// why this has happened, because it should not!
		$output->logCliErrorWithException($e->getMessage(), $e);
		exit(1);
	}

	// ====================================================================
	//
	// PARSE THE COMMAND-LINE
	//
	// --------------------------------------------------------------------

	// find our version number from Composer
	$myVersion = new MyVersion('datasift/storyplayer');

	// create the engine
	$engine = new CliEngine($injectables->defaultCommandLine);

	// set the main metadata
	//
	// you CAN use this constant in your stories - we'll maintain it
	// going forward!
	define('STORYPLAYER_MAJOR_VERSION', '2');
	$engine->setAppName('storyplayer');
	$engine->setAppVersion((string)$myVersion);
	$engine->setAppUrl('https://datasift.github.io/storyplayer/');
	$engine->setAppCopyright('Copyright (c) 2012-present MediaSift Ltd. All rights reserved.');
	$engine->setAppLicense('Released under the BSD 3-Clause license');

	// add the switches that come before any of the commands
	// if we have any
	//
	// add switches in the order of precedence
	$engine->addEngineSwitch(new VersionSwitch);
	$engine->addEngineSwitch(new KillProcesses_Switch);
	$engine->addEngineSwitch(new ListProcesses_Switch);
	$engine->addEngineSwitch(new ListSystems_Switch($injectables->knownSystemsUnderTestFilenames));
	$engine->addEngineSwitch(new ListTargets_Switch($injectables->knownTestEnvironmentsList));
	$engine->addEngineSwitch(new LongHelpSwitch);
	$engine->addEngineSwitch(new ShortHelpSwitch);

	// what is our default command?
	// this is normally some sort of help command
	$engine->setDefaultCommand(new PlayStory_Command($injectables));

	// add any additional commands
	// note - we do not have to add the default command a 2nd time
	// note - if we add no additional commands, the default command is implied
	//        and the user does not have to type it on the command line
	$engine->addCommand(new BuildTestEnvironment_Command($injectables));
	$engine->addCommand(new HelpCommand);
	$engine->addCommand(new CreateStory_Command);
	$engine->addCommand(new Install_Command);
	$engine->addCommand(new ListHostsTable_Command);
	$engine->addCommand(new Script_Command($injectables));
	$engine->addCommand(new ShowTestEnvironment_Command);

	// all set - let's go
	try {
		return $engine->main($argv, $injectables);
	}
	catch (Exception $e)
	{
		echo "*** fatal error: " . $e->getMessage() . "\n\n";

		if (get_class($e) !== 'Exception') {
			echo "Stack trace is:\n" . $e->getTraceAsString() . "\n";
		}
	}
}

return main($argv);
