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

/**
 * Bart Nagel <bjn@ecs.soton.ac.uk>
 */

require dirname(dirname(__FILE__)) . "/setup.php";

// sort out commandline options
require dirname(dirname(__FILE__)) . "/lib/clioptions/CliOptions.class.php";
$opts = new CliOptions();
$opts->add(null, "help");
$opts->add(null, "classifier", CliOption::TYPE_MULTIPLEVALUE, null, "Use the given classifier rather than using all classifiers. Can be used more than once to pick multiple classifiers.");
$opts->add(null, "output-metadata", CliOption::TYPE_SWITCH, null, "Output metadata as it is found");
$opts->add(null, "canonical-paths", CliOption::TYPE_SWITCH, null, "Output canonical paths rather than file IDs");
$opts->add(null, "force", CliOption::TYPE_SWITCH, null, "Skip any cached files and run the classifier anew");
$opts->add(null, "force-missing", CliOption::TYPE_SWITCH, null, "Like --force but only for those missing MBIDs");
$opts->add(null, "id", CliOption::TYPE_SWITCH, null, "Interpret arguments as IDs rather than paths");
$opts->add(null, "threads", CliOption::TYPE_VALUE, "1", "Number of threads to run (default 1)");
$opts->add("l", "list", CliOption::TYPE_SWITCH, null, "List available classifiers and exit");
$opts->add("v", "verbose", CliOption::TYPE_SWITCH, null, "Verbose threads");

function usage($code = 255) {
	global $opts;
	$stream = $code == 0 ? STDOUT : STDERR;
	fwrite($stream, "Usage: " . basename($_SERVER["SCRIPT_NAME"]) . " [options] [<filepath> [...]]\n");
	fwrite($stream, "With no file paths listed all files in the repository will be classified.\n");
	fwrite($stream, "If one or more paths is listed the --output-metadata option is implicit.\n");
	fwrite($stream, "If there were errors classifying any file, exit with an error status (1), otherwise success.\n");
	fwrite($stream, $opts->listopts());
	exit($code);
}

try {
	$options = $opts->getopts();
} catch (CliOptionException $e) {
	fwrite(STDERR, $e->getMessage() . "\n\n");
	usage();
}

// help message if requested
if ($options["help"]) {
	usage(0);
}

// classifier list if requested
if ($options["list"]) {
	echo "Available classifiers:\n\n";
	foreach (allclassifiers() as $classname => $classifier)
		echo "$classname\n\t" . $classifier->getName() . "\n\n\t" . wordwrap($classifier->getDescription(), 70, "\n\t") . "\n\n";
	exit;
}

// number of threads
if (!is_numeric($options["threads"])) {
	fwrite(STDERR, "expected a positive integer as a parameter to the --threads option\n\n");
	usage();
} else {
	$options["threads"] = intVal($options["threads"]);
	if ($options["threads"] < 1) {
		fwrite(STDERR, "expected a positive integer as a parameter to the --threads option\n\n");
		usage();
	}
}

// get list of classifiers to use
if (!empty($options["classifier"])) {
	$classifiers = array();
	foreach ($options["classifier"] as $classname) {
		$classifier = getclassifier($classname);
		if ($classifier === false) {
			fwrite(STDERR, "classifier '" . $options["classifier"] . "' doesn't exist\n");
			exit(255);
		}
		$classifiers[] = $classifier;
	}
} else
	$classifiers = allclassifiers();
echo "using classifiers\n";
foreach ($classifiers as $classifier)
	echo "- " . get_class($classifier) . ": " . $classifier->getName() . "\n";

// get list of files to classify
$repo = new AFRepo();
if (empty($options["_"])) {
	echo "getting list of files to classify...";
	$options["id"] = false;
	$options["_"] = array_keys($repo->getAllFiles());
	echo "done\n";
} else
	$options["output-metadata"] = true;

// loop through files
$fcount = 0;
$ftotal = count($options["_"]);
$ftotalchars = strlen((string) $ftotal);
$errors = array();
$children = array(); // map of child pid to array(ID, classifier)
foreach ($options["_"] as $file) {
	$fcount++;
	if ($options["id"])
		$id = $file;
	else
		$id = $repo->filePathToId($file);

	// loop through classifiers
	foreach ($classifiers as $classifier) {
		// position
		$pos = "[" . sprintf("%0{$ftotalchars}d", $fcount) . "/" . $ftotal . "]";

		// if we're using all threads wait for one to finish
		if (count($children) == $options["threads"]) {
			$pid = pcntl_wait($status);

			// did it error?
			if (pcntl_wexitstatus($status) > 0) {
				// remember what that thread was working on
				list($errorid, $errorclassifier) = $children[$pid];
				if (!isset($errors[$errorid]))
					$errors[$errorid] = array();
				$errors[$errorid][] = $errorclassifier;
			}
			unset($children[$pid]);
		}

		// fork a new thread to work on this file with this classifier
		$pid = pcntl_fork();
		if ($pid == -1) {
			fwrite(STDERR, "could not fork\n");
			exit(8);
		}
		if ($pid) {
			// parent
			$children[$pid] = array($id, $classifier);
		} else {
			// child
			$pid = getmypid();
			if ($options["verbose"])
				echo "thread with pid $pid: working on file $id $pos, classifier " . get_class($classifier) . "\n";

			$out = $pos . "\t";
			if ($options["verbose"])
				$out .= "pid $pid\t";
			$out .= ($options["canonical-paths"] ? $repo->idToCanonicalPath($id) : $id) . "\t";
			$out .= get_class($classifier) . "\t";

			// skip if we're only forcing missing MBIDs and we already have one
			if ($options["force-missing"] && $classifier->hasMBID($id)) {
				$out .= $classifier->getMBID($id);
				echo $out . "\n";
				exit;
			}

			// run classifier if necessory, exit with error status if an error 
			// happens
			$metadata = $classifier->classify($id, $options["force-missing"] || $options["force"]);
			if ($metadata === false) {
				$out .= str_pad("error", 36, "-", STR_PAD_BOTH);
				echo $out . "\n";
				exit(1);
			}

			if ($options["output-metadata"])
				$out .= print_r($metadata, true);
			else {
				$mbid = $classifier->getMBID($id);
				if (is_null($mbid))
					$out .= str_pad("MBID not found", 36, "-", STR_PAD_BOTH);
				else
					$out .= $mbid;
			}
			echo $out . "\n";
			exit;
		}
	}
}

// wait for all threads to finish
echo "waiting for all threads to finish...\n";
while (count($children)) {
	$pid = pcntl_wait($status);

	// did it error?
	if (pcntl_wexitstatus($status) > 0) {
		// remember what that thread was working on
		list($errorid, $errorclassifier) = $children[$pid];
		if (!isset($errors[$errorid]))
			$errors[$errorid] = array();
		$errors[$errorid][] = $errorclassifier;
	}
	unset($children[$pid]);
}

echo "done\n";
if (count($errors)) {
	fwrite(STDERR, "file IDs and classifiers which had errors:\n");
	foreach ($errors as $id => $classifiers)
		fwrite(STDERR, "$id\t " . implode(", ", array_map("get_class", $classifiers)) . "\n");

	exit(1);
}

exit;

?>
