<?php


class Composer_Model extends Database_Model {

	const BASEURL = 'https://getcomposer.org';
	
	private $setupfile;
	private $json;
	static private $object;

	function __construct() {
		parent::__construct();
	}

	static
	public function getInstance() {
		return self::$object;
	}
	
	public function GetInstalledVersion() {
		if (!isset($_SESSION['composer_installed'])) {
			$_SESSION['composer_installed'] = FALSE;
			if (file_exists(HOME . '/composer.phar')) {
				$content = file_get_contents(HOME . '/composer.phar');
				if (preg_match("/^const VERSION = '(.*?)';/m", $content, $matches)) {
					$_SESSION['composer_installed'] = $matches[1];
				}
			}
		}
		return $_SESSION['composer_installed'];
	}
	
	public function GetAvailableVersion() {
		// download `versions`
		if (!isset($_SESSION['composer_available'])) {
			$n = new \CurlObject();
			$n	->Url(self::BASEURL . '/versions')
				->Redirect(1)
				->Execute();
			$body = $n->Body(); 
			$info = json_decode($body, TRUE);
			$_SESSION['composer_available'] = $info['stable'][0];
		}
		return $_SESSION['composer_available'];
	}

	private function _finish($payload) {
		if (file_exists($this->setupfile)) {
			unlink($this->setupfile);
		}
		$json = json_encode($payload);
		echo <<<BLOCK
</pre>
</body>
<script type="text/javascript">
childStatus($json);
</script>
BLOCK;
		flush();
		exit;
	}
	
	private function _header() {
		echo <<<BLOCK
<html>
<head>
<script type="text/javascript">
// send message back to parent
function childStatus(payload) {
	var event = new CustomEvent('pkp', { detail: payload });
	window.parent.document.dispatchEvent(event);
}
</script>
</head>
<body>
<pre>
BLOCK;
	}

/*
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php -r "if (hash_file('sha384', 'composer-setup.php') === '48e3236262b34d30969dca3c37281b3b4bbe3221bda826ac6a9a62d6444cdb0dcd0615698a5cbe587c3f0fe57a54d8f5') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
php composer-setup.php
php -r "unlink('composer-setup.php');"

*/	
	// use their supplied installer - it will check for dependencies
	public function InstallLatestVersion($param) {
		$info = $_SESSION['composer_available'];
		$this->_header();
		$n = new \CurlObject();
		$n	->Url('https://composer.github.io/installer.sig')		// this is the hash in the installation instructions
			->Redirect(1)
			->Execute();
		$filehash = trim($n->Body());

		$this->setupfile = tempnam(sys_get_temp_dir(), 'pkp');
		echo "Downloading installer to {$this->setupfile} ...\n";
		flush();
		
		copy(self::BASEURL . '/installer', $this->setupfile);

		echo "Verifying...\n";
		flush();
		
		$hash = hash_file('sha384', $this->setupfile);
		if ($hash != $filehash) {
			echo "Composer Installer corrupt\n";
			flush();
			return $this->_finish([
				'result' => -1280, 
				'message' => 'Composer Installer corrupt',
			]);
		}
		
		echo "Installing...\n";
		flush();
		
		// php composer-setup.php --install-dir=bin [--filename=composer]
		$argv = [
			$this->setupfile,
			'--install-dir=' . HOME,
		];
		
		self::$object = $this;
		$this->_patchSourceCode();

//		register_shutdown_function([$this, '__shutdown']);
		// add termination function to return status
		// in source code, change exit() to something
	
		include $this->setupfile;
		unset($_SESSION['composer_installed']);
		unset($_SESSION['composer_available']);

		echo "Installation Complete\n";
		flush();
		$dparam = ($param != '') ? '?param=' . rawurlencode($param) : '';

		return $this->_finish([
			'result' => 0, 
			'redirect' => '/admin/composer/dependencies' . $dparam,
		]);
	}
	
	public function __exitCode($value) {
		if ($value) {
			$this->_finish([
				'result' => -1255, 
				'message' => 'Installation script terminated with errors',
			]);
		}
	}
	
	private function _patchSourceCode() {
		// change exit(x) to "return exitCode();"
		$content = file_get_contents($this->setupfile);
		$content = preg_replace_callback('~^\s+exit\((.*?)\)\;~im', function($match) {
			return ' return exitCode(' . $match[1] . ');';
		}, $content);
		file_put_contents($this->setupfile, $content);
	}
	
	// not used
	public function __shutdown() {
		 $this->_finish([
				'result' => -1270, 
				'message' => 'script terminated abnormally',
			]);
	}

	// install missing composer packages, or upgrade existing items
	//  if hash missing or invalid, perform a general update
	public function UpdatePackages($hash) {
		$this->_header();
		if ($hash != '') {
			$payload = $GLOBALS['loader']->GetEphemeral($hash);
			if ($payload === FALSE) {
				return $this->_finish([
					'result' => -1277, 
					'message' => 'ephemeral information has expired. Please retry original operation',
				]);
			} elseif ($payload['action'] != 'composer-package') {
				return $this->_finish([
					'result' => -1277, 
					'message' => 'invalid ephemeral information',
				]);
			}
		} else {
			$payload['list']['require'] = [];
		}

		$composerfile = HOME . '/composer.phar';
		chdir(HOME);

		if (count($payload['list']['require'])) {
			$cmdline = '';
			foreach($payload['list']['require'] as $index => $value) {
				$cmdline .= ' ' . escapeshellarg($value);
			}
			$output = `$composerfile require --update-with-all-dependencies  $cmdline 2>&1`;
			// --prefer-stable --prefer-lowest --update-no-dev
//			$output = `$composerfile show --available $cmdline 2>&1`;
		} else {
			$output = `$composerfile update 2>&1`;
		}
		echo $output;
		flush();

		// make sure all composer packages are installed.
		$required_packages = $this->GetPackageList();
		var_dump($required_packages);
		
		// load the lock file to find out which ones need installing
		$installed = $this->GetComposerLock();
		
		$to_be_installed = [];
		foreach($required_packages as $value => $filler) {
			if (!isset($installed[$value])) {
				$to_be_installed[] = $value;
			}
		}

		if (count($to_be_installed)) {
			echo "Installing the following new packages...\n";
			flush();

			$cmdline = '';
			foreach($to_be_installed as $value) {
				echo "* $value\n";
				$cmdline .= ' ' . escapeshellarg($value);
			}
			flush();
			$output = `$composerfile require --update-with-all-dependencies  $cmdline 2>&1`;
			echo $output;
			flush();
		}

	}
	
	// get a list of all packages needing to be installed
	public function GetPackageList() {
		$dependencylist = [];
		$GLOBALS['loader']->GetDependencies($dependencylist);
		$required_packages = [];
		foreach($dependencylist as $module => $info) {
			if (is_array($info) && isset($info['require'])) {
				foreach($info['require'] as $packagename) {
					$required_packages[$packagename] = 1;
				}
			}
		}
		return $required_packages;
	}
	
	public function GetComposerLock() {
		$result = [];
		$filename = HOME . '/composer.lock';
		if (file_exists($filename)) {
			$content = file_get_contents($filename);
			$json = json_decode($content, TRUE);
			foreach($json['packages'] as $package) {
				$name = $package['name'];
				$version = $package['version'];
				$result[$name] = $version;
			}
		}
		return $result;
	}
	
	public function LoadComposerJson() {
		if (file_exists(HOME . '/composer.json')) {
			$content = file_get_contents(HOME . '/composer.json');
			$this->json = json_decode($content, TRUE);
		} else {
			$this->json = [];
		}
	}
	
	// if this module were to be enabled, what composer packages would need to be installed?
	public function GetMissingPackages($module) {
		$GLOBALS['loader']->Unscanned(FALSE);
		$dependencylist = [];
		$GLOBALS['loader']->GetDependencies($dependencylist, $module);
		$this->LoadComposerJson();
		$missing = [];		// missing packages
		foreach($dependencylist as $module => $info) {
			if (is_array($info) && isset($info['require'])) {
				foreach($info['require'] as $packagename) {
					if (!isset($this->json['require'][$packagename])) {		// has it been installed?
						$missing[$packagename] = 1;
					}
				}
			}
		}
		$missing = array_keys($missing);
		return ['require' => $missing];
	}
	
}

function exitCode($value) {
	$obj = Composer_Model::getInstance();
	$obj->__exitCode($value);
}


