<?php
/*
 * Copyright (C) 2007-2013 Xagasoft, All rights reserved.
 *
 * This file is part of the libgats library and is released under the
 * terms of the license contained in the file LICENSE.
 */


/** @cond */
if( !defined("_phpgats2_defined") )
{
define("_phpgats2_defined",1);
/** @endcond */

/** @mainpage phpgats.php PHP Gats Library
 * @section Notes
 *
 * This library requires that php be built with the The GNU Multiple Precision Arithmetic Library (GMP).
 *
 * If array_values($a)===$a, the object will be encoded as a list, otherwise, it will be encoded as a dictionary.
 *
 * If an integer is > int32_max, it will be delivered as a string.
 *
 * If a string is encodable as an integer without loosing data, it will be.
 *
 * @section Usage
 *
 * function phpgats2_readGats( $str_data (string) );
 * returns mixed
 *
 * function phpgats2_writeGats( $elem (mixed) );
 * returns binary encoded gats string
 *
 */

/**
 * Call this function to generate a binary gats stream from the given php object
 * @param $elem (mixed) the gats element object from which to generate a binary blob
 * @returns (string) binary gats data
 */
function phpgats2_writeGats( $elem )
{
	$str_out = _phpgats2__write( $elem );
	$str_out = "\x01" . pack( "N", strlen($str_out)+5 ) . $str_out;
	return $str_out;
}

/**
 * Call this function to parse a gats binary string into a php type
 * @param $str_data (string) the binary gats data to be parsed
 * @returns (mixed)
 */
function phpgats2_readGats( $str_data )
{
	//print "parsing\n";
	$offset = 0;
	$data_size = strlen( $str_data );
	if( $data_size < 5 )
	{
		throw new Exception( "invalid size (< 5)\n" );
		return false;
	}
	if( ord($str_data) != 1 ) //version
	{
		throw new Exception( "invalid gats version" );
		return false;
	}
	$size = "" . $str_data[1] . $str_data[2] . $str_data[3] . $str_data[4];
	$size = unpack( "Nsize", $size );
	$size = $size["size"];
	if( $data_size < $size )
	{
		throw new Exception( "Not enough data" );
		return false;
	}
	$offset+=5;
	return _phpgats2_parseMaster( $str_data, $offset );
}

/* --- STOP READING --- Below are internal functions you shouldn't call --- */

/**
 * Write a gats packed integer
 * @param $iIn (either a string representing a number, a gmp_int, or an int) The integer to be converted
 * @returns (string) packed int binary data
 */
//TODO::Make this use bcmath
function _phpgats2_packInt( $iIn )
{
	$ret = "";
	if( gmp_cmp( $iIn, 0 ) < 0 )
	{
		$iIn = gmp_mul( $iIn, -1 );
		$b = gmp_intval( gmp_and( $iIn, 0x3f ) );
		if( gmp_cmp( $iIn, $b ) > 0 )
			$b |= 0x80 | 0x40;
		else
			$b |= 0x40;
	}
	else
	{
		$b = gmp_intval( gmp_and( $iIn, 0x3f ) );
		if( gmp_cmp( $iIn, $b ) > 0 )
			$b |= 0x80;
	}

	$ret .= chr( $b );
	$iIn = gmp_div( $iIn, 64 );

	while( gmp_cmp( $iIn, 0 ) > 0 )
	{
		$b = gmp_intval( gmp_and( $iIn, 0x7f ) );
		if( gmp_cmp( $iIn, $b ) > 0 )
			$b |= 0x80;
		$ret .= chr( $b );
		$iIn = gmp_div( $iIn, 128 );
	}

	return $ret;
}

/**
 * Read a gats packed integer into a gmp_int
 * @param $sIn (string) the input stream
 * @param &$pos (integer) the current position in the input stream (will be modified)
 * @returns (gmp_int) the integer
 */
//TODO::Make this use bcmath
function _phpgats2_unpackInt( $sIn, &$pos )
{
	$neg = false;

	$b = ord($sIn[$pos++]);
	if( ($b&0x40) == 0x40 )
		$neg = true;
	$iOut = gmp_init( $b&0x3f );
	$mult = gmp_init( 64 );
	while( ($b&0x80) )
	{
		$b = ord($sIn[$pos++]);
		$iOut = gmp_or( $iOut, gmp_mul( $b&0x7f, $mult ) );
		$mult = gmp_mul( $mult, 128 );
	}
	if( $neg == true )
		$iOut = gmp_mul( $iOut, -1 );

	return $iOut;
}

function _phpgats2_writeBoolean( $b )
{
	return ($b==true)?"1":"0";
}

function _phpgats2_writeString( $s )
{
	$s = (string)$s;
	return 's' . _phpgats2_packInt(strlen($s)) . $s;
}

function _phpgats2_packInteger( $i )
{
	$i = (string)$i;
	return "i" . _phpgats2_packInt(gmp_init(bcmul($i,'1',0)));
}

function _phpgats2_writeFloat( $f )
{
	$f = $f+0.0;
	if( $f == 0.0 )
	{
		return 'Fz';
	}
	else if( is_nan( $f ) )
	{
		return 'Fn';
	}
	else if( is_infinite( $f ) )
	{
		if( $f < 0.0 )
			return 'FI';
		else
			return 'Fi';
	}
	else
	{
		$e = $f;
		$neg = false;
		if( $e < 0.0 )
		{
			$e = -$e;
			$neg = true;
		}
		$iScale = (int)(floor(log($e)/log(256.0)));
		$e = $e/pow(256.0, $iScale);
		$s = chr((int)($e));
		$e = $e - (int)($e);
		for( $j = 0; $j < 150 && $e > 0.0; $j++ )
		{
			$e = $e * 256.0;
			$s .= chr((int)($e));
			$e -= (int)($e);
		}
		$ilen = strlen($s);
		if( $neg ) $ilen = -$ilen;
		return "f" . _phpgats2_packInt($ilen) . $s .
			_phpgats2_packInt($iScale);
	}
}

function _phpgats2_writeList( $l )
{
	$elems = array_values((array)$l);

	$s_out = "l";
	foreach( $elems as $val )
	{
		$s_out .= _phpgats2__write( $val );
	}
	$s_out .= "e";
	return $s_out;
}

function _phpgats2_writeDictionary( $d )
{
	$elems = (array) $d;
	$s_out = "d";
	foreach( $elems as $key => $val )
	{
		$s_out .= _phpgats2_writeString( $key );
		$s_out .= _phpgats2__write( $val );
	}
	$s_out .= "e";
	return $s_out;
}

function _phpgats2__write( $unknown )
{
	if( is_bool( $unknown ) )
	{
		return _phpgats2_writeBoolean( $unknown );
	}
	else if( is_float( $unknown ) )
	{
		return _phpgats2_writeFloat( $unknown );
	}
	else if( is_array( $unknown ) )
	{
		if( array_values($unknown)===$unknown )
			return _phpgats2_writeList( $unknown );
		else
			return _phpgats2_writeDictionary( $unknown );
	}
	else if( is_object( $unknown ) )
	{
		return _phpgats2_writeDictionary( $unknown );
	}
	else if( is_int( $unknown ) )
	{
		return _phpgats2_packInteger( $unknown );
	}
	else if( is_string( $unknown ) )
	{
		if( bcmul( $unknown, "1", 0 ) === $unknown )
			return _phpgats2_packInteger( $unknown );
		return _phpgats2_writeString( $unknown );
	}
}

/**
 * Make sure we can read a character off the input stream
 * @param $str_data (string) the input stream
 * @param $offset (integer) the current position in the input stream
 */
function _phpgats2_parseChar( $str_data, $offset )
{
	if($offset>strlen($str_data))
		throw new Exception("Not enough data");
}

/**
 * Parse a string element out of the input stream
 * @param $str_data (string) the input stream
 * @param &$offset (integer) the current position in the input stream (will be updated)
 * @param $dbg (integer) the current depth (for pretty printing)
 * @returns (string) the element read
 */
function _phpgats2_parseString( $str_data, &$offset, $dbg )
{
	$str_tmp = "";
	$gmpSize = _phpgats2_unpackInt( $str_data, $offset );
	if( gmp_cmp( $gmpSize, 2147483647 ) > 0 )
	{
		throw new Exception(
			"size (" . gmp_strval($gmpSize) . ") > phpgats2 can handle\n");
	}
	$iSize = gmp_intval($gmpSize);
	$i=0;
	$str_tmp = "";
	while( $i<$iSize )
	{
		_phpgats2_parseChar( $str_data, $offset+1 );
		$str_tmp .= $str_data[$offset++];
		++$i;
	}
	return $str_tmp;
}

/**
 * Parse an integer element out of the input stream
 * @param $str_data (string) the input stream
 * @param &$offset (integer) the current position in the input stream (will be updated)
 * @param $dbg (integer) the current depth (for pretty printing)
 * @returns (integer or string) the element read
 */
function _phpgats2_parseInteger( $str_data, &$offset, $dbg )
{
	$gmp = _phpgats2_unpackInt($str_data, $offset);
	$str = gmp_strval($gmp);
	$int = gmp_intval($gmp);
	if( $str === ((string)(int)$int) )
		return $int;
	return $str;
}

/**
 * Parse a float element out of the input stream
 * @param $str_data (string) the input stream
 * @param &$offset (integer) the current position in the input stream (will be updated)
 * @param $dbg (integer) the current depth (for pretty printing)
 * @returns (float) the element read
 */
function _phpgats2_parseFloat( $str_data, &$offset, $dbg )
{
	$str_tmp = "";
	$iSize = gmp_intval(_phpgats2_unpackInt( $str_data, $offset ));
	$neg = false;
	if( $iSize < 0.0 )
	{
		$iSize = -$iSize;
		$neg = true;
	}
	$i=0;
	$str_tmp = "";
	while( $i<$iSize )
	{
		_phpgats2_parseChar( $str_data, $offset+1 );
		$str_tmp .= $str_data[$offset++];
		++$i;
	}
	$iScale = gmp_intval(_phpgats2_unpackInt( $str_data, $offset ));
	$e = 0.0;
	for( $j = $iSize-1; $j > 0; $j-- )
	{
		$e = ($e+ord($str_tmp[$j]))*.00390625;
	}
	$e = ($e+ord($str_tmp[0])) * pow( 256.0, $iScale );
	if( $neg ) $e = -$e;
	return $e;
}

/**
 * Parse a list element out of the input stream
 * @param $str_data (string) the input stream
 * @param &$offset (integer) the current position in the input stream (will be updated)
 * @param $dbg (integer) the current depth (for pretty printing)
 * @returns (array) the element read
 */
function _phpgats2_parseList( $str_data, &$offset, $dbg )
{
	_phpgats2_parseChar( $str_data, $offset );
	$c = $str_data[$offset];
	$l_out = array();
	while( $c != "e" )
	{
		$obj = _phpgats2_parseMaster( $str_data, $offset, $dbg );
		array_push( $l_out, $obj );
		_phpgats2_parseChar( $str_data, $offset );
		$c = $str_data[$offset];
	}
	$offset++;
	return $l_out;
}

/**
 * Parse a dictionary element out of the input stream
 * @param $str_data (string) the input stream
 * @param &$offset (integer) the current position in the input stream (will be updated)
 * @param $dbg (integer) the current depth (for pretty printing)
 * @returns (array) the element read
 */
function _phpgats2_parseDictionary( $str_data, &$offset, $dbg )
{
	_phpgats2_parseChar( $str_data, $offset );
	$c = $str_data[$offset];
	$d_out = array();
	while( $c != "e" )
	{
		$obj1 = _phpgats2_parseMaster( $str_data, $offset, $dbg );
		$obj2 = _phpgats2_parseMaster( $str_data, $offset, $dbg );
		$d_out[$obj1] = $obj2;
		_phpgats2_parseChar( $str_data, $offset );
		$c = $str_data[$offset];
	}
	$offset++;
	return $d_out;
}

/**
 * The internal master recursive parse function
 * @param $str_data (string) the input stream
 * @param &$offset (integer) the current position in the input stream (will be updated)
 * @param $dbg (integer) the current depth (for pretty printing)
 * @returns (mixed) the element read
 */
function _phpgats2_parseMaster( $str_data, &$offset, $dbg=0 )
{
	_phpgats2_parseChar( $str_data, $offset );
	$c = $str_data[$offset++];
	//for( $meme=0; $meme<$dbg; $meme++ )
	//	echo "  ";
	switch( $c )
	{
		case 'i':
			//echo "int:";
			$obj = _phpgats2_parseInteger( $str_data, $offset, $dbg );
			//echo gmp_strval($obj->get()) . "\n";
			return $obj;
			break;
		case 'l':
			//echo "list:\n";
			$obj = _phpgats2_parseList( $str_data, $offset, $dbg+1 );
			return $obj;
			break;
		case 'd':
			//echo "dic:\n";
			$obj = _phpgats2_parseDictionary( $str_data, $offset, $dbg+1 );
			return $obj;
			break;
		case 'f':
			//echo "float:\n";
			$obj = _phpgats2_parseFloat( $str_data, $offset, $dbg );
			return $obj;
			break;
		case 'F':
			_phpgats2_parseChar( $str_data, $offset );
			switch( $str_data[$offset++] )
			{
				case 'Z': return -0.0;
				case 'z': return 0.0;
				case 'N': return -NAN;
				case 'n': return NAN;
				case 'I': return -INF;
				case 'i': return INF;
			}
			break;
		case '1':
			//echo "true\n";
			return true;
			break;
		case '0':
			//echo "false\n";
			return false;
			break;
		default:
			//echo "str:";
			$obj = _phpgats2_parseString( $str_data, $offset, $dbg );
			//echo $obj->get() . "\n";
			return $obj;
			break;
	}
}

/** @cond */
} //defined("_phpgats2_defined");
/** @endcond */