<?php
/*
PiAnoS (Picture Annotation System), A fingerprint examiner training tool
Copyright (C) 2011-2012 University of Lausanne, Switzerland
https://ips-labs.unil.ch/pianos

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see http://www.gnu.org/licenses/.
*/


//header("HTTP/1.1 200 OK");
//header("Content-Type: text/plain; charset=utf-8");
//echo "Test\n";

// Installs an Exception "thrower" for all PHP errors - all of them, including E_NOTICE
// which *are* damn errors, no matter what the PHP user community thinks.
function handleError($errno, $errstr, $errfile, $errline, $errcontext)
{
	throw new Exception("PHP error in ".$errfile."(".$errline."): ".$errstr." (errno=".$errno.")");
}
set_error_handler('handleError');

// Starts an arbitrary process, write $input to stdin, reads the output and waits for
// the process to terminate. Throws exception if anything goes wrong (process not started,
// return value not zero, etc.)
function execCommand($command, $stdin)
{
	$output = NULL;
	$pipes = NULL;
	$process = NULL;
	$ret = NULL;
	
	try
	{
		$descriptors = array(
			0 => array("pipe", "r"),  // stdin is a pipe that the child will read from
			1 => array("pipe", "w"),  // stdout is a pipe that the child will write to
			2 => array("pipe", "w"),  // stderr is a pipe that the child will write to
		);
		$cwd = 'C:\\dev\\svnips\\tswg2\\UNILFingerprintLR\\bin';
		
		$path = getenv('path');
		$path .= 'c:\\dev\\libs\\bin';
		$tswg2 = 'c:\\dev\\svnips\\tswg2';
		$env = array('PATH'=>$path, 'TSWG2_PATH'=>$tswg2);
		
		$process = proc_open($command, $descriptors, $pipes, $cwd, $env);
		if (is_resource($process) && $pipes)
		{
			fwrite($pipes[0], $stdin);
			fclose($pipes[0]);
			
			$output = stream_get_contents($pipes[1]);
			fclose($pipes[1]);
			
			$error = stream_get_contents($pipes[2]);
			fclose($pipes[2]);
			
			$ret = proc_close($process);
		}
		else
			throw new Exception('Could not start the UFLR child process');
	}
	catch (Exception $e)
	{
		if (is_array($pipes))
		{
			foreach ($pipes as $pipe)
				if (is_resource($pipe))
					fclose($pipe);
		}
		
		if (is_resource($process))
			proc_close($process);
			
		throw $e;
	}

	return array($ret,$output);
}

function createMatcherInput($data, &$minutiaeMap)
{
	$buf = '';
	$minutiaeMap = array();
	
	$minNumMinutiae = 2;
	$images = array('mark', 'print');
	// Only those 3 types of minutiae will be considered - centers and differences (and potentially
	// any other type) are silently discarded.
	$types = array('unknown' => 0, 'bifurcation' => 1, 'ridge_ending' => 2);
	
	foreach ($images as $image)
	{
		if (!array_key_exists($image, $data))
			throw new Exception("Invalid input: no ".$image." image found");
			
		$minutiaeMap[$image] = array();
		$index = 0;
		foreach ($data[$image]['minutiae'] as $id=>$m)
		{
			if (!array_key_exists('minutiae', $data[$image]))
				throw new Exception("Invalid input: no minutiae found on the ".$image);
			elseif (count($data[$image]) < $minNumMinutiae)
				throw new Exception("Invalid input: not enough minutiae found on the ".$image." (need at least ".$minNumMinutiae.")");
				
			if (array_key_exists($m['type'], $types))
			{
				$buf .= (float)$m['pos'][0] . ' ' . (float)$m['pos'][1] . ' ' . (float)$m['angle'] . ' ' . (int)$types[$m['type']] . "\n";
				$minutiaeMap[$image][$index++] = $m['id'];
			}
		}
		$buf .= "\n";
	}

	return $buf;
}
function parseOutput($output, $minutiaeMap)
{
	$lines = explode("\n", $output);

	$step = 0;
	
	$numSegments = 0;
	$numPairing = 0;
	
	$ret = array('score' => NULL, 'segments' => array(), 'pairing' => array());
	foreach ($lines as $rawline)
	{
		$line = trim($rawline);

		// The first line should be the score, under the form SCORE = ABSOLUTE / REFERENCE
		if ($step == 0)
		{
			$tokens = explode(" ", $line);
			if (count($tokens) != 5 || $tokens[1] != '=' || $tokens[3] != '/')
				throw new Exception("Could not read the output: score not found");
			
			$ret['score'] = array(
				'normalized' => (float)$tokens[0],
				'absolute' => (float)$tokens[2],
				'reference' => (float)$tokens[4],
			);
			++$step;
		}
		elseif ($step == 1)
		{
			// Then a blank line
			if ($line !== "")
				throw new Exception("Could not read the output: blank line expected");
			++$step;
		}
		elseif ($step == 2)
		{
			// Then X segment pairs
			$tokens = explode(" ", $line);
			if (count($tokens) < 1)
				throw new Exception("Could not read the output: number of segment pairs expected");
			$numSegments = (int)$tokens[0];
			++$step;
		}
		elseif ($step == 3)
		{
			if ($line == "")
			{
				// The segment section is terminated when a blank line is encountered
				// The number of segments read must match the header of the block
				if ($numSegments != count($ret['segments']))
					throw new Exception("Could not read the output: mismatch between number of segments in the block header and segments actually read");
				++$step;
			}
			else
			{
				// Then each segment
				$tokens = explode(" ", $line);
				if (count($tokens) != 5)
					throw new Exception("Could not read the output: unexpected format for a segment pair");
				
				$segment = array();
				$i=0;
				foreach ($tokens as $t)
				{
					if ($i==0)
					{	
						$segment[] = (int)$t;
					}
					else
					{
						$imgIndex = $i < 3 ? 'mark' : 'print';
						$index = (int)$t;
						if (!array_key_exists($index, $minutiaeMap[$imgIndex]))
							throw new Exception("Could not read the output: mismatch between minutia index and ID");
						$segment[] = $minutiaeMap[$imgIndex][$index];
					}
					++$i;
				}
				
				$ret['segments'][] = $segment;
			}
		}
		elseif ($step == 4)
		{
			// Then X associations (minutiae pairing)
			$tokens = explode(" ", $line);
			if (count($tokens) < 1)
				throw new Exception("Could not read the output: number of associations expected");
			$numPairing = (int)$tokens[0];
			++$step;
		}
		elseif ($step == 5)
		{
			if ($line == "")
			{
				// Done !
				if ($numPairing != count($ret['pairing']))
					throw new Exception("Could not read the output: mismatch between number of paired minutiae in the block header and pairs actually read");
				break;
			}
			else
			{
				$tokens = explode(" ", $line);
				if (count($tokens) != 2)
					throw new Exception("Could not read the output: unexpected format for an association");
				
				$pair = array();
				$i=0;
				foreach ($tokens as $t)
				{
					$imgIndex = $i < 1 ? 'mark' : 'print';
					$index = (int)$t;
					if (!array_key_exists($index, $minutiaeMap[$imgIndex]))
						throw new Exception("Could not read the output: mismatch between minutia index and ID");
					$pair[] = $minutiaeMap[$imgIndex][$index];
					++$i;
				}
				$ret['pairing'][] = $pair;
			}
		}
		else
			throw new Exception("Assertion failed");
	}
	
	return $ret;
}

function sendMatcherResult()
{
	// Will hold a map from the index of the minutia as sent to the matcher
	// and its ID. Used later on to translate the output of the matcher, which is
	// indexed-based, to an ID-based map, useful to the client.
	$minutiaeMap = array();
	
	try 
	{
		$data = json_decode(file_get_contents("php://input"), true);
		//if (!$input) $input = array('test'=>'stuff');
		if (!$data)
			throw new Exception("Invalid input data (not JSON ?)");
		
		$matcherInput = createMatcherInput($data, $minutiaeMap);
	} 
	catch (Exception $e) 
	{
		header("Content-Type: application/json; charset=utf-8");
		echo json_encode(array('success'=>false,'message'=>$e->getMessage()));
		exit;
	}
	
	$program = 'C:\\dev\\svnips\\tswg2\\UNILFingerprintLR\\bin\\fpmatch.exe';
	
	$params = array();
	
	$params[] = '--format';
	$params[] = 'pianos';
	
	$params[] = '--xyt-type';
	$params[] = 'uflr';

	$params[] = '--verbosity';
	$params[] = 'full';

	$params[] = '--left-image-height';
	$params[] = (int)$data['mark']['image']['height'];

	$params[] = '--right-image-height';
	$params[] = (int)$data['print']['image']['height'];

	if (isset($_GET['type']) && strlen($_GET['type']))
	{
		$params[] = '--matcher';
		$params[] = $_GET['type'];
	}
	
	if (isset($_GET['maxSegmentLength']) && strlen($_GET['maxSegmentLength']))
	{
		$params[] = '--max-segment-length';
		$params[] = (float)$_GET['maxSegmentLength'];
	}
	if (isset($_GET['distanceThreshold']) && strlen($_GET['distanceThreshold']))
	{
		$params[] = '--distance-threshold';
		$params[] = (float)$_GET['distanceThreshold'];
	}
	if (isset($_GET['angleThreshold']) && strlen($_GET['angleThreshold']))
	{
		$params[] = '--angle-threshold';
		$params[] = (float)$_GET['angleThreshold'];
	}
	
	$command = $program.' ';
	foreach ($params as $p)
		$command .= escapeshellarg($p).' ';


	// No try-catch here as potential errors are purely internal and are actually HTTP 500's
	$cmdres = execCommand($command, $matcherInput);
	$ret = $cmdres[0];
	$output = $cmdres[1];
	
	
	try 
	{
		if ($ret != 0)
			throw new Exception($output);
			
		$parsed = parseOutput($output, $minutiaeMap);
	} 
	catch (Exception $e) 
	{
		header("Content-Type: application/json; charset=utf-8");
		echo json_encode(array('success'=>false,'message'=>$e->getMessage()));
		exit;
	}
	
	$ret = array('success'=>true, 'ret'=>$parsed/*, 'raw'=>$output*/);
	
	$content = json_encode($ret);

	header("Content-Type: application/json; charset=utf-8");
	header("Content-Length: ".strlen($content));
	echo $content;
	exit;
}


// main() function, all enclosed in a try-catch block.
try
{	
	// if (!isset($_GET['action']))
		// $_GET['action'] = 'match';
		
	if (!isset($_GET['action']))
		throw new Exception("Invalid parameters");

	switch ($_GET['action'])
	{
	case 'match':
		
		sendMatcherResult();
		
		break;
	
	default:
		throw new Exception("Invalid parameters");
	}
}
catch (Exception $e)
{
	// Unhandled exceptions raise an HTTP 500 error, showing a stack trace. If you think this is
	// a security risk, you are right - and you should fix the error. We need the HTTP 500 and not
	// the HTTP 200 with a JSON object with 'success' set to false, because in the general case
	// we don't know what data type the client expects (it may be an image, or a JSON object, or
	// text, or whatever).
	header("HTTP/1.1 500 Internal Server Error");
	header("Content-Type: text/plain; charset=utf-8");
	echo "The server encountered an unexpected error. We are sorry for the inconvenience.\nDetails about the error follow.\n\n";
	echo "Message: ".$e->getMessage()."\n";
	echo "File: ".$e->getFile()."(".$e->getLine().")\n";
	echo "Stack trace:\n";
	echo $e->getTraceAsString();
	echo "\n\n";
	
	echo "GET parameters:\n";
	var_dump($_GET);
	echo "\n";
	
	echo "POST parameters:\n";
	var_dump($_POST);
}

?>
