<?php
/**
 * AMFDeserializer takes the raw amf input stream and converts it PHP objects
 * representing the data.
 * 
 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
 * @copyright (c) 2003 amfphp.org
 * @package flashservices
 * @subpackage io
 * @version $Id$
 */

include_once(AMFPHP_BASE . "amf/io/AMFBaseDeserializer.php");

class AMFDeserializer extends AMFBaseDeserializer {
	/**
	 * The raw data input
	 * 
	 * @access private 
	 * @var string 
	 */
	var $raw_data;

	/**
	 * The current seek cursor of the stream
	 * 
	 * @access private 
	 * @var int 
	 */
	var $current_byte;

	/**
	 * The length of the stream.  Since this class is not actually using a stream
	 * the entire content of the stream is passed in as the initial argument so the
	 * length can be determined.
	 * 
	 * @access private 
	 * @var int 
	 */
	var $content_length;
	
	/**
	 * The number of headers in the packet.
	 * 
	 * @access private 
	 * @var int 
	 */
	var $header_count;

	/**
	 * The content of the packet headers
	 * 
	 * @access private 
	 * @var string 
	 */
	var $headers;

	/**
	 * The number of bodys in the packet.
	 * 
	 * @access private 
	 * @var int 
	 */
	var $body_count;

	/**
	 * The content of the body elements
	 * 
	 * @access private 
	 * @var string 
	 */
	var $body;

	/**
	 * The object to store the amf data.
	 * 
	 * @access private 
	 * @var object 
	 */
	var $amfdata;

	/**
	 * The instance of the amfinput stream object
	 * 
	 * @access private 
	 * @var object 
	 */
	var $inputStream;
	
	/**
	 * metaInfo
	 */
	var $meta;
	
	var $storedStrings;
	var $storedObjects;
	var $storedDefinitions;
	var $amf0storedObjects;
	
	var $native;

	/**
	 * Constructor method for the deserializer.  Constructing the deserializer converts the input stream
	 * content to a AMFObject.
	 * 
	 * @param object $is The referenced input stream
	 */
	function AMFDeserializer($rd) {
		AMFBaseDeserializer::AMFBaseDeserializer($rd);
	} 

	/**
	 * readObject reads the name/value properties of the amf message and converts them into
	 * their equivilent php representation
	 * 
	 * @return array The php array with the object data
	 */
	function readObject() {
		$ret = array(); // init the array
		$this->amf0storedObjects[] = & $ret;
		$key = $this->readUTF(); // grab the key
		for ($type = $this->readByte(); $type != 9; $type = $this->readByte()) {
			$val = $this->readData($type); // grab the value
			$ret[$key] = $val; // save the name/value pair in the array
			$key = $this->readUTF(); // get the next name
		}
		return $ret; // return the array
	} 
	
	/**
	 * readMixedObject reads the name/value properties of the amf message and converts
	 * numeric looking keys to numeric keys
	 * 
	 * @return array The php array with the object data
	 */
	function readMixedObject() {
		$ret = array(); // init the array
		$this->amf0storedObjects[] = & $ret;
		$key = $this->readUTF(); // grab the key
		for ($type = $this->readByte(); $type != 9; $type = $this->readByte()) {
			$val = $this->readData($type); // grab the value
			if(is_numeric($key))
			{
				$key = (float) $key;
			}
			$ret[$key] = $val; // save the name/value pair in the array
			$key = $this->readUTF(); // get the next name
		}
		return $ret; // return the array
	} 

	/**
	 * readArray turns an all numeric keyed actionscript array into a php array.
	 * 
	 * @return array The php array
	 */
	function readArray() {
		$ret = array(); // init the array object
		$this->amf0storedObjects[] = & $ret;
		$length = $this->readLong(); // get the length  of the array
		for ($i = 0; $i < $length; $i++) { // loop over all of the elements in the data
			$type = $this->readByte();  // grab the type for each element
			$ret[] = $this->readData($type); // grab each element
		} 
		return $ret; // return the data
		
	} 

	/**
	 * readMixedArray turns an array with numeric and string indexes into a php array
	 * 
	 * @return array The php array with mixed indexes
	 */
	function readMixedArray() {
		//$length   = $this->readLong(); // get the length  property set by flash
		$this->current_byte += 4;
		return $this->readMixedObject(); // return the body of mixed array
	} 

	/**
	 * readCustomClass reads the amf content associated with a class instance which was registered
	 * with Object.registerClass.  In order to preserve the class name an additional property is assigned
	 * to the object "_explicitType".  This property will be overwritten if it existed within the class already.
	 * 
	 * @return object The php representation of the object
	 */
	function readCustomClass() {
		$typeIdentifier = str_replace('..', '', $this->readUTF());
		$obj = $this->mapClass($typeIdentifier);
		$isObject = true;
		if($obj == NULL)
		{
			$obj = array();
			$isObject = false;
		}
		$this->amf0storedObjects[] = & $obj;
		$key = $this->readUTF(); // grab the key
		for ($type = $this->readByte(); $type != 9; $type = $this->readByte()) {
			$val = $this->readData($type); // grab the value
			if($isObject)
			{
				$obj->$key = $val; // save the name/value pair in the array
			}
			else
			{
				$obj[$key] = $val; // save the name/value pair in the array
			}
			$key = $this->readUTF(); // get the next name
		}
		if(!$isObject)
		{
			$obj['_explicitType'] = $typeIdentifier;
		}
		return $obj; // return the array
	} 

	/**
	 * readDate reads a date from the amf message and returns the time in ms.
	 * This method is still under development.
	 * 
	 * @return long The date in ms.
	 */
	function readDate() {
		$ms = $this->readDouble();  // date in milliseconds from 01/01/1970
		$int = $this->readInt(); // nasty way to get timezone
		if ($int > 720) {
			$int = - (65536 - $int);
		} 
		$int *= -60;
		//$int *= 1000;
		//$min = $int % 60;
		//$timezone = "GMT " . - $hr . ":" . abs($min);
		// end nastiness
		
		//We store the last timezone found in date fields in the request
		//FOr most purposes, it's expected that the timezones
		//don't change from one date object to the other (they change per client though)
		DateWrapper::setTimezone($int);
		return $ms; 
	}

	/**
	 * readReference replaces the old readFlushedSO. It treats where there
	 * are references to other objects. Currently it does not resolve the
	 * object as this would involve a serious amount of overhead, unless
	 * you have a genius idea
	 * 
	 * @return String 
	 */
	function readReference() {
		$reference = $this->readInt();
		return $this->amf0storedObjects[$reference];
	} 
	
	function readAny()
	{
	     if($this->native)
	         return amfphp_decode($this->raw_data, $this->decodeFlags, $this->current_byte, array(& $this, "decodeCallback"));
	     else
	     {
	 		$type = $this->readByte();  // grab the type of the element
	 		return $this->readData($type); // turn the element into real data
	     }
	}

	/**
	 * readData is the main switch for mapping a type code to an actual
	 * implementation for deciphering it.
	 * 
	 * @param mixed $type The $type integer
	 * @return mixed The php version of the data in the message block
	 */
	function readData($type) {
		switch ($type) {
			case 0: // number
				$data = $this->readDouble();
				break;
			case 1: // boolean
				$data = $this->readByte() == 1;
				break;
			case 2: // string
				$data = $this->readUTF();
				break;
			case 3: // object Object
				$data = $this->readObject();
				break;
			case 5: // null
				$data = null;
				break;
			case 6: // undefined
				$data = null;
				break;
			case 7: // Circular references are returned here
				$data = $this->readReference();
				break;
			case 8: // mixed array with numeric and string keys
				$data = $this->readMixedArray();
				break;
			case 10: // array
				$data = $this->readArray();
				break;
			case 11: // date
				$data = $this->readDate();
				break;
			case 12: // string, strlen(string) > 2^16
				$data = $this->readLongUTF();
				break;
			case 13: // mainly internal AS objects
				$data = null;
				break;
			case 15: // XML
				$data = $this->readLongUTF();
				break;
			case 16: // Custom Class
				$data = $this->readCustomClass();
				break;
			case 17: //AMF3-specific
				$GLOBALS['amfphp']['encoding'] = "amf3";
				$data = $this->readAmf3Data();
				break;
			default: // unknown case
				trigger_error("Found unhandled type with code: $type");
				exit();
				break;
		} 
		return $data;
	} 
	
	/********************************************************************************
	 *                       This is the AMF3 specific stuff
	 ********************************************************************************/
	function readAmf3Data()
	{
		$type = $this->readByte();
		switch($type)
		{
			case 0x00 : return null; //undefined
			case 0x01 : return null; //null
			case 0x02 : return false; //boolean false
			case 0x03 : return true;  //boolean true
			case 0x04 : return $this->readAmf3Int();
			case 0x05 : return $this->readDouble();
			case 0x06 : return $this->readAmf3String();
			case 0x07 : return $this->readAmf3XmlString();
			case 0x08 : return $this->readAmf3Date();
			case 0x09 : return $this->readAmf3Array();
			case 0x0A : return $this->readAmf3Object();
			case 0x0B : return $this->readAmf3XmlString();
			case 0x0C : return $this->readAmf3ByteArray();
			default: trigger_error("undefined Amf3 type encountered: " . $type, E_USER_ERROR);
		}
	}
	
	/// <summary>
	/// Handle decoding of the variable-length representation
	/// which gives seven bits of value per serialized byte by using the high-order bit 
	/// of each byte as a continuation flag.
	/// </summary>
	/// <returns></returns>
	function readAmf3Int()
	{
		$int = $this->readByte();
		if($int < 128)
			return $int;
		else
		{
			$int = ($int & 0x7f) << 7;
			$tmp = $this->readByte();
			if($tmp < 128)
			{
				return $int | $tmp;
			}
			else
			{
				$int = ($int | ($tmp & 0x7f)) << 7;
				$tmp = $this->readByte();
				if($tmp < 128)
				{
					return $int | $tmp;
				}
				else
				{
					$int = ($int | ($tmp & 0x7f)) << 8;
					$tmp = $this->readByte();
					$int |= $tmp;
					
	            	// Check if the integer should be negative
	            	if (($int & 0x10000000) != 0) {
	            		// and extend the sign bit
	            		$int |= 0xe0000000;
	            	}
	            	return $int;
				}
			}
		}
	}

    function readAmf3Date() 
    {
        $dateref = $this->readAmf3Int();
        if (($dateref & 0x01) == 0) {
            $dateref = $dateref >> 1;
            if ($dateref>=count($this->storedObjects)) {
                trigger_error('Undefined date reference: ' . $dateref, E_USER_ERROR);
                return false;
            }
            return $this->storedObjects[$dateref];
        }
        //$timeOffset = ($dateref >> 1) * 6000 * -1;
        $ms = $this->readDouble();

        //$date = $ms-$timeOffset;
        $date = $ms;
        
        $this->storedObjects[] = & $date;
        return $date;
    }

    /**
     * readString 
     * 
     * @return string 
     */
    function readAmf3String() {

        $strref = $this->readAmf3Int();

        if (($strref & 0x01) == 0) {
            $strref = $strref >> 1;
            if ($strref >= count($this->storedStrings)) {
                trigger_error('Undefined string reference: ' . $strref, E_USER_ERROR);
                return false;
            }
            return $this->storedStrings[$strref];
        } else {
            $strlen = $strref >> 1; 
            $str = "";
            if ($strlen > 0) 
            {
            	$str = $this->readBuffer($strlen);
            	$this->storedStrings[] = $str;
            }
            return $str;
        }

    }

	function readAmf3XmlString()
	{
		$handle = $this->readAmf3Int();
		$inline = (($handle & 1) != 0 ); $handle = $handle >> 1;
		if( $inline )
		{
			$xml = $this->readBuffer($handle);
			$this->storedStrings[] = $xml;
		}
		else
		{
			$xml = $this->storedObjects[$handle];
		}
		return $xml;
	}

	function readAmf3ByteArray()
	{
		$handle = $this->readAmf3Int();
		$inline = (($handle & 1) != 0 );$handle = $handle >> 1;
		if( $inline )
		{
			$ba = new ByteArray($this->readBuffer($handle));
			$this->storedObjects[] = $ba;
		}
		else
		{
			$ba = $this->storedObjects[$handle];
		}
		return $ba;
	}

	function readAmf3Array()
	{
		$handle = $this->readAmf3Int();
		$inline = (($handle & 1)  != 0 ); $handle = $handle >> 1;
		if( $inline )
		{
			$hashtable = array();
			$this->storedObjects[] = & $hashtable;
			$key = $this->readAmf3String();
			while( $key != "" )
			{
				$value = $this->readAmf3Data();
				$hashtable[$key] = $value;
				$key = $this->readAmf3String();
			}

			for($i = 0; $i < $handle; $i++)
			{
				//Grab the type for each element.
				$value = $this->readAmf3Data();
				$hashtable[$i] = $value;
			}
			return $hashtable;
		}
		else
		{
			return $this->storedObjects[$handle];
		}
	}

	function readAmf3Object()
	{
		$handle = $this->readAmf3Int();
		$inline = (($handle & 1) != 0 ); $handle = $handle >> 1;
		
		if( $inline )
		{
			//an inline object
			$inlineClassDef = (($handle & 1) != 0 );$handle = $handle >> 1;
			if( $inlineClassDef )
			{
				//inline class-def
				$typeIdentifier = $this->readAmf3String();
				$typedObject = !is_null($typeIdentifier) && $typeIdentifier != "";
				//flags that identify the way the object is serialized/deserialized
				$externalizable = (($handle & 1) != 0 );$handle = $handle >> 1;
				$dynamic = (($handle & 1) != 0 );$handle = $handle >> 1;
				$classMemberCount = $handle;
				
				$classMemberDefinitions = array();
				for($i = 0; $i < $classMemberCount; $i++)
				{
					$classMemberDefinitions[] = $this->readAmf3String();
				}
				//string mappedTypeName = typeIdentifier;
				//if( applicationContext != null )
				//	mappedTypeName = applicationContext.GetMappedTypeName(typeIdentifier);

				$classDefinition = array("type" => $typeIdentifier, "members" => $classMemberDefinitions,
										"externalizable" => $externalizable, "dynamic" => $dynamic);
				$this->storedDefinitions[] = $classDefinition;
			}
			else
			{
				//a reference to a previously passed class-def
				$classDefinition = $this->storedDefinitions[$handle];
			}
		}
		else
		{
			//an object reference
			return $this->storedObjects[$handle];
		}		
		
		
		$type = $classDefinition['type'];
		$obj = $this->mapClass($type);
		
		$isObject = true;
		if($obj == NULL)
		{
			$obj = array();
			$isObject = false;
		}

		//Add to references as circular references may search for this object
		$this->storedObjects[] = & $obj;

		if( $classDefinition['externalizable'] )
		{
			if($type == 'flex.messaging.io.ArrayCollection')
			{
				$obj = $this->readAmf3Data();
			}
			else if($type == 'flex.messaging.io.ObjectProxy')
			{
				$obj = $this->readAmf3Data();
			}
			else
			{
				trigger_error("Unable to read externalizable data type " . $type, E_USER_ERROR);
			}
		}
		else
		{
			$members = $classDefinition['members'];
			$memberCount = count($members);
			for($i = 0; $i < $memberCount; $i++)
			{
				$val = $this->readAmf3Data();
				$key = $members[$i];
				if($isObject)
				{
					$obj->$key = $val;
				}
				else
				{
					$obj[$key] = $val;
				}
			}

			if($classDefinition['dynamic']/* && obj is ASObject*/)
			{
				$key = $this->readAmf3String();
				while( $key != "" )
				{
					$value = $this->readAmf3Data();
					if($isObject)
					{
						$obj->$key = $value;
					}
					else
					{
						$obj[$key] = $value;
					}
					$key = $this->readAmf3String();
				}
			}
			
			if($type != '' && !$isObject)
			{
				$obj['_explicitType'] = $type;
			}
		}
		
		if($isObject && method_exists($obj, 'init'))
		{
			$obj->init();
		}
		
		return $obj;
	}
    
    /**
     * Taken from SabreAMF
     */
    function readBuffer($len)
    {
    	$data = substr($this->raw_data,$this->current_byte,$len);
    	$this->current_byte += $len;
    	return $data;
    }
} 

?>