<?php
require_once('SyncML/Constants.php');
class Atmail_SyncML_SyncMLClient
{
	
	public $result = null; // store for last method results or errors
	public $debug = false;
	public $echoOn = false;
	public $logIO = true;
	
	private $_currentStage = 1;
	private $_Stage1Done = false; //flags to track client server comms through the sync Stages
	private $_Stage2Done = false;
	private $_Stage3Done = false;
	
	//This class also contains the XML processing methods so some of the class vars are related to processing the XML responses etc.
	private $stackLocal = array();
	private $stackLocalCommands = array(); //register all the local commands here so can be handled by subsequent responses
	private $stackRemoteCommands = array();
	private $MsgIDLocal = 1;
	
	private $responseNodes = array();
	private $responseNodesElementPointerCurrent = null;
	private $responseNodesElementPointerParentsStack = array();
	
	public function __construct($args = array())
	{
		
		if( !array_key_exists('url', $args) )
			$this->setResult( false, 'no URL specified', 1);
		
		if( !array_key_exists('username', $args) )
			$this->setResult( false, 'no username specified', 2);
		
		if( !array_key_exists('password', $args) )
			$this->setResult( false, 'no password specified', 4);
		
		$this->url = $args['url'];
		$this->username = $args['username'];
		$this->password = $args['password'];
		$devid = substr('AtmailClient-' . md5($args['localUsername'] . $this->username),0,32); //add server ip or other server unique val to make safe for multiple Atmail clients to access same server - max 32 chars - cant use password, else store id will get changed if user changes password. Currently keyed to username.
		$this->charset = 'UTF-8';
		$this->state = new SyncML_State($devid, $this->username, time());
		$this->state->mode = 'CLIENT';
		$this->state->setVersion(2);
		$this->state->authType = 'md5';
		$this->state->targetURI = $args['url'];
		$this->state->wbxml = false;
		$state->serverInfo = array('serverTZ' => "+0800");
		
		$this->responseNodesElementPointerCurrent =& $this->responseNodes;
		$this->responseNodesElementPointerParentsStack[] = &$this->responseNodes;
		
		$this->_backend = new Atmail_SyncML_Backend_Atmailclient( array('backendMode' => SYNCML_BACKENDMODE_CLIENT, 'syncDeviceID' => $devid, 'username' => $args['localUsername']) );
		$this->startGreen = "\033[1;32m";
		$this->endGreen = "\033[0;39m";
		$this->startRed = "\033[1;31m";
		$this->endRed = "\033[0;39m";
		$this->state->next = time();
		
		$storedAnchors = $this->_backend->readSyncAnchors( 'contacts' );
		if( $storedAnchors === false )
		{
			
			$this->state->anchorLocalLast = 0; //UNIX GMT
			$this->state->anchorLocalNext = $this->state->next; //UNIX GMT
			$this->state->anchorRemoteLast = null; //UNIX GMT
			$this->state->anchorRemoteNext = null; //UNIX GMT
			
		}
		else
		{
			
			$this->state->anchorLocalLast = $storedAnchors[0];
			$this->state->anchorLocalNext = $this->state->next;
			$this->state->anchorRemoteLast = $storedAnchors[1];
			$this->state->anchorRemoteNext = null;
			
		}
		$this->state->remoteDateFormat = 'UNIX';
		$this->state->remoteTimeZone = 'Asia/Kuala_Lumpur';
		
		
	}
	
	public function sync()
	{
		
		//Stage 1
		//create anchors record if !exist
		//get anchors                    
		//create session ID
		//create XML SyncML packet, 
		//send it and get response
		//unpack
		//look for needed info, MaxObject size etc.
		$success = $this->doStage1();
		if( !$success )
			return false;
		$this->_currentStage = 2;
		$success = $this->doStage2();
		if( !$success )
			return false;
		$this->_currentStage = 3;
		$success = $this->doStage3();
		if( !$success )
			return false;
		$this->_currentStage = 4;
		$success = $this->doStage4();
		if( !$success )
			return false;
		if( $this->echoOn )
			die(__METHOD__ . ' #' . __LINE__);
		
		return true;
		
	}
	
	private function doStage1() 
	{

		$this->_xmlWriter = &Atmail_SyncML_XMLOutput::singleton();
		$this->_xmlWriter->init( new XML_WBXML_ContentHandler() );
		$this->_xmlWriter->outputInit();
		$this->_xmlWriter->outputHeader( array('password' => $this->password, 'MetaMaxMsgSizeEnabled' => false, 'NextNonce' => null));
		//set response we are waiting for.                                                                                                                                              
		$this->stackLocalCommands[] = array('MsgRef' => $this->state->messageID, 'CmdRef' => 0, 'Cmd' => 'SyncHdr', 'handled' => false); 
		$this->_xmlWriter->outputBodyStart();                                                                                   
		$this->_xmlWriter->outputFinal();
		//$this->_xmlWriter->opaque('<Final></Final>');
		$this->_xmlWriter->outputEnd();
		
		$output = $this->_xmlWriter->getOutput();
		$output = '<?xml version="1.0" encoding="UTF-8"?>' . $output;
		
		if( $this->logIO )
			Zend_Registry::get('log')->imap( $this->startGreen . __METHOD__ . ' #' . __LINE__ . ": \$output \n" . str_replace('><',">\n<", $output) . $this->endGreen );
		if( $this->echoOn )
			echo '<pre><font color="green">Local Stage 1 output: ' . "\n" . htmlentities(str_replace('><',">\n<", $output), ENT_QUOTES, 'UTF-8') . "</font></pre>\n";
		$response = $this->requestAndResponse($output);
		if( $this->logIO )
			Zend_Registry::get('log')->imap( $this->startRed . __METHOD__ . ' #' . __LINE__ . ": \$response \n" . str_replace('><',">\n<", $response) . $this->endRed);
		if( $this->echoOn )
			echo '<pre><font color="red">Remote Stage 1 input: ' . "\n" . htmlentities(str_replace('><',">\n<", $response), ENT_QUOTES, 'UTF-8') . "</font></pre>\n";
		
		//reinit output writer so ready to start rendering response from response handlers
		$this->_xmlWriter->init( new XML_WBXML_ContentHandler() );

		
		/* try to extract charset from XML text */
		if( preg_match('/^\s*<\?xml[^>]*encoding\s*=\s*"([^"]*)"/i', $response, $m) )
		    $this->charset = $m[1];
		
		$parser = xml_parser_create_ns($this->charset);
		xml_set_object($parser, $this);
		xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false);
		xml_set_element_handler($parser, '_handleStartElement', '_handleEndElement');
		xml_set_character_data_handler($parser, '_characters');
		xml_set_processing_instruction_handler($parser, false);
		xml_set_external_entity_ref_handler($parser, false);
		
		if( !xml_parse($parser, $response) ) 
		{
		
		    $s = sprintf('XML error: %s at line %d', xml_error_string(xml_get_error_code($parser)), xml_get_current_line_number($parser));                                         
		    Zend_Registry::get('log')->error( $s );
		    $this->setResult(false, $s, 1);
			xml_parser_free($parser);
		    return false;
		} 
		
		xml_parser_free($parser);	
		
		$this->_Stage1Done = true;
		return true;
		
	}
	
	private function doStage2()
	{
		
		//Zend_Registry::get('log')->debug( "\n" . print_r($this->stackRemoteCommands, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$this->stackRemoteCommands \n");
		//Zend_Registry::get('log')->debug( "\n" . print_r($this->state, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$this->state \n");
		
		if( !$this->_Stage1Done )
		{
			$this->_error = 'Stage 1 not done';
			return false;
		}
		$this->state->messageID++;
		
		$this->_xmlWriter = &Atmail_SyncML_XMLOutput::singleton();
		$this->_xmlWriter->init( new XML_WBXML_ContentHandler() );
		$this->_xmlWriter->outputInit();
		$this->_xmlWriter->outputHeader( array('MetaMaxMsgSizeEnabled' => false, 'password' => $this->password, 'NextNonce' => $this->state->NextNonce));
		
		//set response we are waiting for (auth still accepted - also trigger to update NextNonce).                                                                                                                                              
		$this->stackLocalCommands[] = array('MsgRef' => $this->state->messageID, 'CmdRef' => 0, 'Cmd' => 'SyncHdr', 'handled' => false); 
		
		$this->_xmlWriter->outputBodyStart();                                                                                   
		
		//output header status CmdRef = 0
		$this->_xmlWriter->outputStatus(0, 'SyncHdr', 200, $this->state->targetURI, $this->state->sourceURI);
		//UnixGMTToISO8601TZ( time(), 'Asia/Kuala_Lumpur')
		$LastISO8601Locale = UnixGMTToISO8601TZ( $this->state->anchorLocalLast, $this->state->remoteTimeZone);
		$NextISO8601Locale = UnixGMTToISO8601TZ( $this->state->anchorLocalNext, $this->state->remoteTimeZone);
		
		$lastCmdID = $this->_xmlWriter->outputAlert(200, 'contacts', 'contacts', $LastISO8601Locale, $NextISO8601Locale);
		//Sent Alert so Await Status in response
		$this->stackLocalCommands[] = array('MsgRef' => $this->state->messageID, 'CmdRef' => $lastCmdID, 'Cmd' => 'Alert', 'handled' => false);
		
		//$lastCmdID = $this->_xmlWriter->outputGetDevInf();
		//$this->stackLocalCommands[] = array('MsgRef' => $this->state->messageID, 'CmdRef' => $lastCmdID, 'Cmd' => 'GetDevInf', 'handled' => false);
		
		//$this->_xmlWriter->outputFinal();
		$this->_xmlWriter->opaque('<Final></Final>');
		
		$this->_xmlWriter->outputEnd();
		
		$output = $this->_xmlWriter->getOutput();
		$output = '<?xml version="1.0" encoding="UTF-8"?>' . $output;
		
		if( $this->logIO )
			Zend_Registry::get('log')->imap( $this->startGreen . __METHOD__ . ' #' . __LINE__ . ": \$output \n" . str_replace('><',">\n<", $output) . $this->endGreen);
		if( $this->echoOn )
			echo '<pre><font color="green">Local Stage 2 output: ' . "\n" . htmlentities(str_replace('><',">\n<", $output), ENT_QUOTES, 'UTF-8') . "</font></pre>\n";
		$response = $this->requestAndResponse(trim($output));
		if( $this->logIO )
			Zend_Registry::get('log')->imap( $this->startRed . __METHOD__ . ' #' . __LINE__ . ": \$response \n" . str_replace('><',">\n<", $response) . $this->endRed);
		if( $this->echoOn )
			echo '<pre><font color="red">Remote Stage 2 input: ' . "\n" . htmlentities(str_replace('><',">\n<", $response), ENT_QUOTES, 'UTF-8') . "</font></pre>\n";
		
		//reinit output writer so ready to start rendering response from response handlers
		$this->_xmlWriter->init( new XML_WBXML_ContentHandler() );
		
		
		/* try to extract charset from XML text */
		$parser = xml_parser_create_ns($this->charset);
		xml_set_object($parser, $this);
		xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false);
		xml_set_element_handler($parser, '_handleStartElement', '_handleEndElement');
		xml_set_character_data_handler($parser, '_characters');
		xml_set_processing_instruction_handler($parser, false);
		xml_set_external_entity_ref_handler($parser, false);
		
		//reinitialize the stack
		$this->_Stack = array();
		
		if( !xml_parse($parser, $response) ) 
		{
		
		    $s = sprintf('XML error: %s at line %d', xml_error_string(xml_get_error_code($parser)), xml_get_current_line_number($parser));                                         
		    Zend_Registry::get('log')->error( $s );
		    $this->setResult(false, $s, 1);
			xml_parser_free($parser);
		    return false;
		} 
		
		xml_parser_free($parser);	
		$this->_Stage2Done = true;
		return true;
		
	}
	
	private function doStage3()
	{
		
		if( !$this->_Stage2Done )
			throw new Exception( 'stage 2 not completed first' );

		$this->state->messageID++;
		
		$this->_xmlWriter = &Atmail_SyncML_XMLOutput::singleton();
		$this->_xmlWriter->init( new XML_WBXML_ContentHandler() );
		$this->_xmlWriter->outputInit();
		$this->_xmlWriter->outputHeader( array('MetaMaxMsgSizeEnabled' => false, 'password' => $this->password, 'NextNonce' => $this->state->NextNonce));
		
		//set response we are waiting for (auth still accepted - also trigger to update NextNonce).                                                                                                                                              
		$this->stackLocalCommands[] = array('MsgRef' => $this->state->messageID, 'CmdRef' => 0, 'Cmd' => 'SyncHdr', 'handled' => false); 
		
		
		$this->_xmlWriter->outputBodyStart();                                                                                   
		//output header status CmdRef = 0
		$this->_xmlWriter->outputStatus(0, 'SyncHdr', 200, $this->state->targetURI, $this->state->sourceURI);
		
		
		//Zend_Registry::get('log')->debug( "\n" . print_r($this->stackRemoteCommands, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \stackRemoteCommands \n");
		//output statuses for the last remote commands
		//Zend_Registry::get('log')->debug( "\n" . print_r($this->stackRemoteCommands, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$this->stackRemoteCommands \n");
		
		foreach( $this->stackRemoteCommands as $k => $stackRemoteCommand )
		{
			
			//if( $this->debug )
			//	Zend_Registry::get('log')->debug( "\n" . print_r($stackRemoteCommand, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$stackRemoteCommand \n");
			
			$success = $this->produceStatusOutputFromCommand( $stackRemoteCommand );
			if( !$success )
				return false;                       
			//only unset if not SyncHdr
			if( $stackRemoteCommand['CmdRef'] != 'SyncHdr' )
				unset( $this->stackRemoteCommands[$k] );
			
		}
		/*
		
		//output results for the last remote commands
		foreach( $this->stackRemoteCommands as &$stackRemoteCommand )
		{
			
			$success = $this->produceResultsOutputFromCommand( $stackRemoteCommand );
			if( !$success )
				return false;            
			//remove now handled command from the queue
			unset( $stackRemoteCommand );
		}
		*/                                                        
		$this->syncCommandsLocal =  array(  );
		$adds = $mods = $dels = array();
		$this->_backend->getLocalChanges('contacts', $this->state->anchorLocalLast, $this->state->anchorLocalNext, $adds, $mods, $dels );
		if( $this->debug )
			Zend_Registry::get('log')->debug( "\n" . print_r($adds, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$adds \n");
		if( $this->debug )
			Zend_Registry::get('log')->debug( "\n" . print_r($mods, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$mods \n");
		if( $this->debug )
			Zend_Registry::get('log')->debug( "\n" . print_r($dels, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$dels \n");
		
		foreach( $adds as $localID => $remoteID ) //$remoteID irrelevant for client mode
		{
			
			$contactVcard = $this->_backend->retrieveEntry( 'contacts', $localID );
			//Zend_Registry::get('log')->debug( "\n" . print_r($contactVcard, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$contactVcard \n");
			
			$this->syncCommandsLocal[] = array('MsgRef' => $this->state->messageID, 'Cmd' => 'Add', 'CmdRef' => null, 'LocURI' => $localID, 'Data' => $contactVcard);
			
		}
		
		foreach( $mods as $localID => $remoteID ) //$remoteID irrelevant for client mode
		{
			
			$contactVcard = $this->_backend->retrieveEntry( 'contacts', $localID );
			//Zend_Registry::get('log')->debug( "\n" . print_r($contactVcard, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$contactVcard \n");
			
			$this->syncCommandsLocal[] = array('MsgRef' => $this->state->messageID, 'Cmd' => 'Replace', 'CmdRef' => null, 'LocURI' => $localID, 'Data' => $contactVcard);
			
		}
		
		foreach( $dels as $localID => $remoteID ) //$remoteID irrelevant for client mode
		{
			
			$this->syncCommandsLocal[] = array('MsgRef' => $this->state->messageID, 'Cmd' => 'Delete', 'CmdRef' => null, 'LocURI' => $localID);
			
		}
		
		$cmdID = $this->produceSyncOutput( $this->syncCommandsLocal );
		if( !$success )
			return false;
		$this->stackLocalCommands[] = array('MsgRef' => $this->state->messageID, 'CmdRef' => $cmdID, 'Cmd' => 'Sync', 'handled' => false); 

		//$this->_xmlWriter->outputFinal();
		$this->_xmlWriter->opaque('<Final></Final>');
		
		$this->_xmlWriter->outputEnd();
		
		$output = $this->_xmlWriter->getOutput();
		$output = '<?xml version="1.0" encoding="UTF-8"?>' . $output;
		
		if( $this->logIO )
			Zend_Registry::get('log')->imap( $this->startGreen . __METHOD__ . ' #' . __LINE__ . ": \$output \n" . str_replace('><',">\n<", $output) . $this->endGreen);
		if( $this->echoOn )
			echo '<pre><font color="green">Local Stage 3 output: ' . "\n" . htmlentities(str_replace('><',">\n<", $output), ENT_QUOTES, 'UTF-8') . "</font></pre>\n";
		$response = $this->requestAndResponse(trim($output));
		
		if( $this->logIO )
			Zend_Registry::get('log')->imap( $this->startRed . __METHOD__ . ' #' . __LINE__ . ": \$response \n" . str_replace('><',">\n<", $response) . $this->endRed);
		if( $this->echoOn )
			echo '<pre><font color="red">Remote Stage 3 input: ' . "\n" . htmlentities(str_replace('><',">\n<", $response), ENT_QUOTES, 'UTF-8') . "</font></pre>\n";
		//die(__METHOD__ . ' #' . __LINE__);

		//reinit output writer so ready to start rendering response from response handlers
		$this->_xmlWriter->init( new XML_WBXML_ContentHandler() );
		
		
		/* try to extract charset from XML text */
		$parser = xml_parser_create_ns($this->charset);
		xml_set_object($parser, $this);
		xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false);
		xml_set_element_handler($parser, '_handleStartElement', '_handleEndElement');
		xml_set_character_data_handler($parser, '_characters');
		xml_set_processing_instruction_handler($parser, false);
		xml_set_external_entity_ref_handler($parser, false);
		
		//reinitialize the stack
		$this->_Stack = array();
		
		//initialize _map for new items response
		$this->_map = array();
		//TODO: possibly scrape old map data for duplicate ids before discarding in a 508 slow sync.
		if( !xml_parse($parser, $response) ) 
		{
		
		    $s = sprintf('XML error: %s at line %d', xml_error_string(xml_get_error_code($parser)), xml_get_current_line_number($parser));                                         
		    Zend_Registry::get('log')->error( $s );
		    $this->setResult(false, $s, 1);
			xml_parser_free($parser);
		    return false;
		} 
		
		xml_parser_free($parser);	
		
		
		//Zend_Registry::get('log')->debug( "\n" . print_r($this->_map, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$this->_map \n");
		
		$this->_Stage3Done = true;
		return true;
		
	}
	
	private function doStage4() //response to server changes and map response
	{
		
		
		if( !$this->_Stage3Done )
			throw new Exception( 'stage 3 not completed first' );

		$this->state->messageID++;
		
		$this->_xmlWriter = &Atmail_SyncML_XMLOutput::singleton();
		$this->_xmlWriter->init( new XML_WBXML_ContentHandler() );
		$this->_xmlWriter->outputInit();
		$this->_xmlWriter->outputHeader( array('MetaMaxMsgSizeEnabled' => false, 'password' => $this->password, 'NextNonce' => $this->state->NextNonce));
		
		//set response we are waiting for (auth still accepted - also trigger to update NextNonce).                                                                                                                                              
		$this->stackLocalCommands[] = array('MsgRef' => $this->state->messageID, 'CmdRef' => 0, 'Cmd' => 'SyncHdr', 'handled' => false); 
		
		$this->_xmlWriter->outputBodyStart();
		
		//output header status CmdRef = 0
		$this->_xmlWriter->outputStatus(0, 'SyncHdr', 200, $this->state->targetURI, $this->state->sourceURI);
		
		//Samsung requested that Sync Status be sent first.
		$this->stackRemoteCommands = array_reverse($this->stackRemoteCommands);
		
		
		foreach( $this->stackRemoteCommands as $k => $stackRemoteCommand )
		{
			
			$success = $this->produceStatusOutputFromCommand( $stackRemoteCommand );
			if( !$success )
				return false;
			//only unset if not SyncHdr
			if( $stackRemoteCommand['CmdRef'] != 'SyncHdr' )
				unset( $this->stackRemoteCommands[$k] );
			
		}
		if( $this->debug )
			Zend_Registry::get('log')->debug( "\n" . print_r($this->_map, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$this->_map \n");
		
		if( count($this->_map) > 0 )
		{
			$cmdID = $this->_xmlWriter->outputMap('contacts', 'contacts', $this->_map);
			$this->stackLocalCommands[] = array('MsgRef' => $this->state->messageID, 'CmdRef' => $cmdID, 'Cmd' => 'Map', 'handled' => false); 
		}
		
		//$this->_xmlWriter->outputFinal();
		$this->_xmlWriter->opaque('<Final></Final>');
		
		$this->_xmlWriter->outputEnd();
		
		$output = $this->_xmlWriter->getOutput();
		$output = '<?xml version="1.0" encoding="UTF-8"?>' . $output;
		
		if( $this->logIO )
			Zend_Registry::get('log')->imap( $this->startGreen . __METHOD__ . ' #' . __LINE__ . ": \$output \n" . str_replace('><',">\n<", $output) . $this->endGreen);
		if( $this->echoOn )
			echo '<pre><font color="green">Local Stage 4 output: ' . "\n" . htmlentities(str_replace('><',">\n<", $output), ENT_QUOTES, 'UTF-8') . "</font></pre>\n";
		$response = $this->requestAndResponse(trim($output));
		if( $this->logIO )
			Zend_Registry::get('log')->imap( $this->startRed . __METHOD__ . ' #' . __LINE__ . ": \$response \n" . str_replace('><',">\n<", $response) . $this->endRed);
		if( $this->echoOn )
			echo '<pre><font color="red">Remote Stage 4 input: ' . "\n" . htmlentities(str_replace('><',">\n<", $response), ENT_QUOTES, 'UTF-8') . "</font></pre>\n";
		
		//reinit output writer so ready to start rendering response from response handlers
		$this->_xmlWriter->init( new XML_WBXML_ContentHandler() );
		
		
		/* try to extract charset from XML text */
		$parser = xml_parser_create_ns($this->charset);
		xml_set_object($parser, $this);
		xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false);
		xml_set_element_handler($parser, '_handleStartElement', '_handleEndElement');
		xml_set_character_data_handler($parser, '_characters');
		xml_set_processing_instruction_handler($parser, false);
		xml_set_external_entity_ref_handler($parser, false);
		
		//reinitialize the stack
		$this->_Stack = array();
		
		if( !xml_parse($parser, $response) ) 
		{
		
		    $s = sprintf('XML error: %s at line %d', xml_error_string(xml_get_error_code($parser)), xml_get_current_line_number($parser));                                         
		    Zend_Registry::get('log')->error( $s );
		    $this->setResult(false, $s, 1);
			xml_parser_free($parser);
		    return false;
		} 
		
		xml_parser_free($parser);	
		
		//TODO: make sure all local commands have been answered, and all remote commands have been handled and andwered.
		
		$this->_backend->writeSyncAnchors( 'contacts', $this->state->anchorLocalNext, $this->state->anchorRemoteNext );
		
		//Zend_Registry::get('log')->debug( "\n" . print_r($this->_map, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$this->_map \n");
		
		$this->_Stage4Done = true;
		return true;
		
	}

	
	
	
	
	private function _handleStartElement($parser, $tag, $attributes)
	{
		
		list($uri, $element) = $this->_splitURI($tag);
		$this->_Stack[] = $element;
		
		if( is_array($this->responseNodesElementPointerCurrent) )
			$this->responseNodesElementPointerCurrent[$element] = '';
		else
			$this->responseNodesElementPointerCurrent = array($element => ''); //array unless chars overwrites with data.
		$this->responseNodesElementPointerCurrent = &$this->responseNodesElementPointerCurrent[$element];
		$this->responseNodesElementPointerParentsStack[] = &$this->responseNodesElementPointerCurrent;
		
	}
	
	private function _handleEndElement($parser, $tag)
	{
		
		list($uri, $element) = $this->_splitURI($tag);
        
		if( count($this->_Stack) == 2 )
		{

 			if( $this->_Stack[1] == 'SyncHdr')
            	$this->handleSyncHdrInResponse( $this->responseNodesElementPointerCurrent );
			
			//nothing to do if find </SyncBody>

		}
        
        //if the node being closed had text data then add this to the response array before processing closing
		if( isset($this->_chars) && strlen(trim($this->_chars)) > 0 )
		{
			
			$this->responseNodesElementPointerCurrent = trim($this->_chars);
			unset($this->_chars);                       
			
		}
		
        if( $this->_Stack[1] == 'SyncBody' )
		{
            switch( count($this->_Stack) )
			{
            case 2:
                // </SyncBody> end of SyncBody. Finish everything:
                break;
            case 3:
                
				// </[Command]></SyncBody></SyncML>
                // Command finished. Complete parsing and pass on to Handler
				if( $element == 'Status' )
					$this->handleStatusInRemoteInput( $this->responseNodesElementPointerCurrent );
				elseif( $element == 'Alert' )
					$this->handleAlertInRemoteInput( $this->responseNodesElementPointerCurrent );
				elseif( $element == 'Get' )
					$this->handleGetInRemoteInput( $this->responseNodesElementPointerCurrent ); //prob a get for main data like devinfo
				elseif( $element == 'Results' )
					$this->handleResultsInRemoteInput( $this->responseNodesElementPointerCurrent );
				elseif( $element == 'Sync' )
					$this->handleSyncInRemoteInput( $this->responseNodesElementPointerCurrent );
				break;
				
            case 4:
                
				if( $this->_Stack[2] == 'Sync' )// child sub-nodes we are interested in
				{
					                                        
					if( $element == 'CmdID' || $element == 'Target' || $element == 'Source' || $element == 'NumberOfChanges' )
						break;
					elseif( $element == 'Add' )
						$this->handleSyncAddInRemoteInput( $this->responseNodesElementPointerCurrent ); //prob a get for main data like devinfo
					elseif( $element == 'Replace' )
						$this->handleSyncReplaceInRemoteInput( $this->responseNodesElementPointerCurrent ); //prob a get for main data like devinfo
					elseif( $element == 'Delete' )
						$this->handleSyncDeleteInRemoteInput( $this->responseNodesElementPointerCurrent ); //prob a get for main data like devinfo
					else
					{
						
						if( $this->debug )
							Zend_Registry::get('log')->debug( "\n" . print_r($this->_Stack, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$this->_Stack \n");
						$this->setResult(false, 'Unahandled Sync sub-command: ' . $element, 256);
						
					}
					break;
				}
			default:
                // </...></[Command]></SyncBody></SyncML>
                //some nodes ending on this level could be new records in a sync node, so handle here individually. 
				//(this eliminates the need to upgrade the node array to cope with multiple records with the same element name.)
				//still finishing processing of Stage 1 so leave as TODO
				//Zend_Registry::get('log')->debug( __METHOD__ . ' #' . __LINE__ . 'unhandled closing \$element' . $element . ', in ' . print_r($this->_Stack,true));
				
                break;
            }
        }
        array_pop( $this->responseNodesElementPointerParentsStack );
        $this->responseNodesElementPointerCurrent = &$this->responseNodesElementPointerParentsStack[ count($this->responseNodesElementPointerParentsStack)-1];
        array_pop($this->_Stack);
		
	}
	
	
	
	private function handleSyncAddInRemoteInput( &$responseNode )
	{
		
		//Zend_Registry::get('log')->debug( "\n" . print_r($responseNode, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$responseNode \n");
		$localIDnew = $this->_backend->addEntry('contacts', $responseNode['Item']['Data'], $responseNode['Meta']['Type']);
		//add data for return map of new local ids
		$this->_map[] = array('Remote' => $responseNode['Item']['Source']['LocURI'], 'Local' => $localIDnew);
		$this->stackRemoteCommands[] = array('Cmd' => 'Add', 'Node' => $responseNode, 'handled' => true);
		return true;
		
	}
	
	private function handleSyncReplaceInRemoteInput( &$responseNode )
	{
		
		//Zend_Registry::get('log')->debug( "\n" . print_r($responseNode, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$responseNode \n");
		$luid = $this->_backend->replaceEntry('contacts', $responseNode['Item']['Data'], $responseNode['Meta']['Type'], $responseNode['Item']['Target']['LocURI']);
		//local id might need to change so update in response.
		
		if( $luid != $responseNode['Item']['Target']['LocURI'] )
		{

			$status = '201';
			//$responseNode['Item']['Target']['LocURI'] = $luid;
			$this->_map[] = array('Remote' => $responseNode['Item']['Source']['LocURI'], 'Local' => $luid);
			
			
		}
		else
			$status = '200';
		$this->stackRemoteCommands[] = array('Cmd' => 'Replace', 'Node' => $responseNode, 'handled' => true, 'status' => $status);
		return true;
		
	}
	
	private function handleSyncDeleteInRemoteInput( &$responseNode )
	{
		
		$success = $this->_backend->deleteEntry('contacts', $responseNode['Item']['Target']['LocURI']);
		if( $this->debug )
			Zend_Registry::get('log')->debug( __METHOD__ . ' #' . __LINE__ . ': $success=' . ($success?'deleted':'NOT deleted'));
		if( $this->debug )
			Zend_Registry::get('log')->debug( "\n" . print_r($responseNode, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$responseNode \n");
		
		if( $success )
			$status = '200'; //Item not deleted (e.g. not found)
		else
			$status = '211';
		if( $this->debug )
			Zend_Registry::get('log')->debug( __METHOD__ . ' #' . __LINE__ . ': $status=' . $status);
		//die('<hr/><pre>' . __METHOD__ . ' #' . __LINE__ . ': $status <br />' . htmlentities(print_r($status, true),null,'utf-8') . '</pre>');
		
		$this->stackRemoteCommands[] = array('Cmd' => 'Delete', 'Node' => $responseNode, 'handled' => true, 'status' => $status);
		return $success;
	}
	
	
	
	
	private function produceStatusOutputFromCommand( &$commandNode )
	{
		
		$command = $commandNode['Cmd'];
		$node = $commandNode['Node'];
		if( $this->debug )
			//Zend_Registry::get('log')->debug( "\n" . print_r($commandNode, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$commandNode \n");
		
		if( $command == 'Alert' ) //server wants to know something
		{
			
			if( $node['Item']['Source']['LocURI'] == 'contacts')
			{

				//if alert contains item then remote is wanting to know metrics on the store
				//convert remote datestamps to UNIX GMT if necessary
				
				if( strlen( $node['Item']['Meta']['Anchor']['Last']) == 14 )
					$this->state->remoteDateFormat = 'ISO-8601';
				
				//Kim requested we force respond 200
				if( true )
					$data = '200';
				elseif( $this->state->anchorRemoteLast === null || $this->state->anchorLocalLast === null )
					//no record found so this is the first sync, of sync lost, so issue a 508 refresh status
					$data = '508';
				elseif( $remoteDateFormat == 'ISO-8601' && $this->state->anchorRemoteLast == ISO8601TZToUnixGMT( $node['Item']['Meta']['Anchor']['Last'], $this->state->remoteTimeZone) )
					$data = '200'; //two-way sync???
				else
					$data = '201'; //slow-sync

				//update so that can be stored at end of successful sync
				$this->state->anchorRemoteLast = ISO8601TZToUnixGMT( $node['Item']['Meta']['Anchor']['Last'], $this->state->remoteTimeZone);
				$this->state->anchorRemoteNext = ISO8601TZToUnixGMT( $node['Item']['Meta']['Anchor']['Next'], $this->state->remoteTimeZone);
				
				$this->_xmlWriter->outputStatus( $node['CmdID'], $command, $data, 
						//TODO: slightly different anchor response requirements depending on recponse code 200(two-way), 201(slow)
						$node['Item']['Source']['LocURI'], $node['Item']['Target']['LocURI'],
						$node['Item']['Meta']['Anchor']['Next'], //TODO: compare against stored anchors to detemine sync anchor
						($data=='200'?$node['Item']['Meta']['Anchor']['Last']:null)//only 2way needs Last
				);
				
			}
		}
		elseif( $command == 'Get')
		{
			if( $node['Item']['Target']['LocURI'] == './devinf12' ) //asking for device information
				$this->_xmlWriter->outputStatus( $node['CmdID'], $command, 200);
			
		}
		else
		{
			
			if( $commandNode['handled'] == true )
			{
				
				//Zend_Registry::get('log')->debug( "\n" . print_r($commandNode, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$commandNode \n");
				//Zend_Registry::get('log')->debug( "\n" . print_r($node, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$node \n");
				
				$target = (isset($node['Source']['LocURI'])?$node['Source']['LocURI']:(isset($node['Item']['Source']['LocURI'])?$node['Item']['Source']['LocURI']:null));
				$source = (isset($node['Target']['LocURI'])?$node['Target']['LocURI']:(isset($node['Item']['Target']['LocURI'])?$node['Item']['Target']['LocURI']:null));
				if( array_key_exists('status', $commandNode) )
					$status = $commandNode['status'];
				else
					$status = ($command=='Replace'?'200':'200');
				Zend_Registry::get('log')->debug( __METHOD__ . ' #' . __LINE__ . ': $status=' . $status);
				$this->_xmlWriter->outputStatus( $node['CmdID'], $command, $status, $source, $target);
				
			}
			
		}
		//TODO: remove commandNodes as handled and compleeted responding to server (dont send it again, unless needed in SyncML packet)
		$commandNode['handled'] = true;
		return true;
		
		
	}                              
	
	private function produceResultsOutputFromCommand( $commandNode )
	{
		
		$command = $commandNode['Cmd'];
		$node = $commandNode['Node'];
		Zend_Registry::get('log')->debug( "\n" . print_r($command, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$command \n");
		Zend_Registry::get('log')->debug( "\n" . print_r($node, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$node \n");
		//return true;
		
		if( $command == 'Get' )
		{
			
			if( $node['Item']['Target']['LocURI'] == './devinf12' )
			{
				
				$this->_xmlWriter->outputDevInf( array('CmdRef' => $node['CmdID']) );
				
			}
			
		}
		return true;
	}
	
	private function produceSyncOutput( &$syncCommands )
	{
		
		$cmdID = $this->_xmlWriter->outputSyncStart( 'contacts', 'contacts' );
		foreach( $syncCommands as $k => $command )
			$syncCommands[$k]['CmdRef'] = $this->_xmlWriter->outputSyncCommand($command['Cmd'], $command['Data'], ($command['Cmd']!='Delete'?'text/vcard':null), null, $command['LocURI']);
		$this->_xmlWriter->outputSyncEnd();
		return $cmdID;
		
	}
	
	public function testAccount()
	{
		
		$body = $this->requestAndResponse('');
		Zend_Registry::get('log')->debug( "\n" . print_r($body, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$body \n");
		return true;
		
	}
	
	private function handleSyncHdrInResponse( $responseNode )
	{
		                                                          
		//Zend_Registry::get('log')->debug( "\n" . print_r($responseNode, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$responseNode \n");
		
		//$this->state->sessionId = $responseNode['SessionID'];
		//TODO: use other infor provided by server like MaxObjSize
		//nothing to do
		
	}
	
	private function handleStatusInRemoteInput( &$responseNode )
	{
		
		//search through commands to find matching then handle appropriately
		//Zend_Registry::get('log')->debug( "\n" . print_r($responseNode, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$responseNode \n");
		
		$foundMatchingRequest = false;
		//status response can also be from sync sub commands so search them first
		if( $responseNode['Cmd'] == 'Add' || $responseNode['Cmd'] == 'Replace' || $responseNode['Cmd'] == 'Delete')
		{
			
			//Zend_Registry::get('log')->debug( "\n" . print_r($responseNode, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$responseNode \n");
			//Zend_Registry::get('log')->debug( "\n" . print_r($this->syncCommandsLocal, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$this->syncCommandsLocal \n");
			
			
			foreach( $this->syncCommandsLocal as &$syncCommand )
			{
				
				if( 
					$syncCommand['MsgRef'] == $responseNode['MsgRef'] &&
					$syncCommand['CmdRef'] == $responseNode['CmdRef'] && 
					$syncCommand['Cmd'] == $responseNode['Cmd']
				)
				{
					
					$foundMatchingRequest = true;
					$matchingRequest = &$syncCommand;
					
				}
				
			}
			
		}
		else
		{
			foreach( $this->stackLocalCommands as &$stackLocalCommand )
			{
			
				if( 
					$stackLocalCommand['MsgRef'] == $responseNode['MsgRef'] && 
					$stackLocalCommand['CmdRef'] == $responseNode['CmdRef'] && 
					$stackLocalCommand['Cmd'] == $responseNode['Cmd'] 
				)
				{
					$foundMatchingRequest = true;
					$matchingRequest = &$stackLocalCommand;
					//Zend_Registry::get('log')->debug( __METHOD__ . ' #' . __LINE__ . ": Found a matching command response");
				
				}
			}
		}                                             
		if( !$foundMatchingRequest )
		{
			Zend_Registry::get('log')->debug( "\n" . print_r($this->stackLocalCommands, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$this->stackLocalCommands \n");
			
			Zend_Registry::get('log')->debug( __METHOD__ . ' #' . __LINE__ . ': No matching request found for this response Node: $responseNode= ' . print_r($responseNode, true) );
			$this->setResult(false, 'Warning processing sync response, extra unexpected  data received', 16 );
			return false;
		}
			
		//Zend_Registry::get('log')->debug( "\n" . print_r($matchingRequest, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$matchingRequest \n");
		//auth status response
		if( $matchingRequest['Cmd'] == 'SyncHdr' )
			$this->handleAuthStatusInRemoteInput( $responseNode );
		elseif( $matchingRequest['Cmd'] == 'Alert' )
		{
			
			if( $responseNode['Data'] == '200' )
			{
				
				$this->state->syncType = 'two-way';
				$from = $responseNode['Item']['Data']['Anchor']['Last']; //UNIX timestap therefore GMT (confirmed appears to be GMT)
				$to = $responseNode['Item']['Data']['Anchor']['Next']; //UNIX timestap therefore GMT (confirmed appears to be GMT) 
				Zend_Registry::get('log')->debug( __METHOD__ . ' Previous Sync state found on server. two-way sync selected.');
				
			}
			elseif( $responseNode['Data'] == '508' )
			{
				
				$this->state->syncType = 'slow';
				$from = $responseNode['Item']['Data']['Anchor']['Last']; //UNIX timestap therefore GMT (confirmed appears to be GMT)
				$to = $responseNode['Item']['Data']['Anchor']['Next']; //UNIX timestap therefore GMT (confirmed appears to be GMT) 
				//Zend_Registry::get('log')->debug( "\n" . print_r($responseNode['Item']['Data']['Anchor'], true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$responseNode['Item']['Data']['Anchor'] \n");
				
				Zend_Registry::get('log')->debug( __METHOD__ . ' Previous Sync state not found on server. slow sync selected.');
				Zend_Registry::get('log')->debug( __METHOD__ . ' #' . __LINE__ . ': From: ' . date('r', $from) . " to " . date('r', $to) );
				
			}
			elseif( $responseNode['Data'] == '000' )
			{
			
				$this->state->syncType = 'slow';
				Zend_Registry::get('log')->debug( __METHOD__ . ' #' . __LINE__ . ': Contacts store not found on server: ' . print_r($responseNode,true) );
				$this->setResult( 'Contacts store not found on server');
				return false;
			
			}
			else
			{
				
				$this->state->syncType = 'slow';
				Zend_Registry::get('log')->debug( __METHOD__ . ' #' . __LINE__ . ': Forcing slow sync. Unknown response code: ' . print_r($responseNode,true) );
				//$this->setResult( false, 'Forcing slow sync. Unknown response code: ' . $responseNode['Data']);
				
			}
		}
		elseif( $matchingRequest['Cmd'] == 'Sync' )
		{
			if( $responseNode['Data'] != 200 )
			{
				$this->setResult( false, 'Sync Failed with code: ' . $responseNode['Data'], 64);
				return false;
			}
		}
		elseif( $matchingRequest['Cmd'] == 'Map' )
		{
			if( $responseNode['Data'] != 200 )
			{
				$this->setResult( false, 'Map Failed with code: ' . $responseNode['Data'], 64);
				return false;
			}
		}
		$matchingRequest['handled'] = true; //mark as handled so further steps dont fail expecting it
		
		
		//match up with expected data and process with any set flags for expected data
		
	}   
	
	private function handleAlertInRemoteInput( $responseNode )
	{
		
		//Zend_Registry::get('log')->debug( "\n" . print_r($responseNode, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$responseNode \n");
		
		//the only alert we expect in stage 1 is a server side anchor exchange
		$this->stackRemoteCommands[] = array('Cmd' => 'Alert', 'Node' => $responseNode, 'handled' => false);
		
		
		
		
		
	}
	
	private function handleGetInRemoteInput( $responseNode )
	{
		
		Zend_Registry::get('log')->debug( "\n" . print_r($responseNode, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$responseNode \n");
		$this->stackRemoteCommands[] = array('Cmd' => 'Get', 'Node' => $responseNode, 'handled' => false);
		
	}
	
	private function handleDeleteInRemoteInput( $responseNode )
	{
		
		Zend_Registry::get('log')->debug( "\n" . print_r($responseNode, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$responseNode \n");
		$this->stackRemoteCommands[] = array('Cmd' => 'Delete', 'Node' => $responseNode, 'handled' => false);
		
	}
	
	private function handleResultsInRemoteInput( $responseNode )
	{
		
		$foundMatchingRequest = false;
		foreach( $this->stackLocalCommands as $stackLocalCommand )
		{
			
			if( 
				$stackLocalCommand['MsgRef'] == $responseNode['MsgRef'] && 
				$stackLocalCommand['CmdRef'] == $responseNode['CmdRef'] 
				//$stackLocalCommand['Cmd'] == $responseNode['Cmd'] 
			)
			{
				$foundMatchingRequest = true;
				$matchingRequest = $stackLocalCommand;
				Zend_Registry::get('log')->debug( __METHOD__ . ' #' . __LINE__ . ": Found a matching command response");
				
			}
		}                                             
		if( !$foundMatchingRequest )
		{

			Zend_Registry::get('log')->debug( __METHOD__ . ' #' . __LINE__ . ': No matching request found for this response Node: $responseNode= ' . print_r($responseNode, true) );
			Zend_Registry::get('log')->debug( "\n" . print_r($this->stackLocalCommands, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$this->stackLocalCommands \n");
			$this->setResult(false, 'Warning processing sync response, extra unexpected  data received', 16 );
			return false;

		}
			
		//Zend_Registry::get('log')->debug( "\n" . print_r($responseNode, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$matchingRequest \n");
		//Zend_Registry::get('log')->debug( "\n" . print_r($matchingRequest, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$matchingRequest \n");
		
		if( $matchingRequest['Cmd'] == 'GetDevInf' )
		{
			
			$state->serverInfo = $responseNode['Item']['Data']['DevInf'];
			$state->serverInfo['serverTZ'] = "+0800";
			$matchingRequest['handled'] = true;
			
		}
		else
		{
			
			throw new Exception( __METHOD__ . '#' . __LINE__ . ': Unhandled command: ' . $matchingRequest['Cmd']);
			return false;
			
		}   
		
		return true;
		
		
	}
	
	private function handleSyncInRemoteInput( $responseNode )
	{
		
		$this->stackRemoteCommands[] = array('Cmd' => 'Sync', 'Node' => $responseNode, 'handled' => true);
		return true;
		
	}   
	
	private function handleAuthStatusInRemoteInput( $responseNode )
	{
		
		//Zend_Registry::get('log')->debug( "\n" . print_r($responseNode, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$responseNode \n");
		
		if( $responseNode['Data'] == '212' )
			$this->state->authenticated = true;
		else
			$this->setResult(false, 'Invalid credentials', 8);
		if( array_key_exists('NextNonce', $responseNode['Chal']['Meta']) )
			$this->state->NextNonce = base64_decode($responseNode['Chal']['Meta']['NextNonce']);
			
	}

	//utility function to do the HTTP call to the SyncML server
	private function requestAndResponse($body, $method = 'POST')
	{
		
		$client = new Zend_Http_Client($this->url);
		$client->setConfig( array(
			'keepalive' => true, 
			'useragent'	=> 'Atmail SyncML Client 1.0'));
		$client->setRawData($body);
		$client->setHeaders('Content-Type', 'application/vnd.syncml+xml');
		//$client->setEncType('text/xml');
		$client->setMethod($method);
		$response = $client->request();
		return $response->getBody();

	}

    function getOutput()
    {
        return $this->_xmlWriter->getOutput();
    }

    function _characters($parser, $chars)
    {

		//Zend_Registry::get('log')->debug( __METHOD__ . ' #' . __LINE__ . ': ' . $chars);
		$this->_chars .= $chars;

    }

    function _splitURI($tag)
    {
        $parts = explode(':', $tag);
        $name = array_pop($parts);
        $uri = implode(':', $parts);
        return array($uri, $name);
    }

	private function setResult( $success = false, $message = 'Error', $messageId = 0 )
	{
		
		$this->result = array( 'success' => $success, 'message' => $message, 'messageId' => $messageId );
		
	}                
	
	public function getResult()
	{
		
		return $this->result;
		
	}

}