<?php
/**
 * This file is part of the WURFL implementation of the DDR Simple API and
 * contains the implementation of the {@link Service} interface as an concrete
 * implementation of the {@link BasicService} abstract class.
 *
 * @author Sylvain Lequeux
 * @author Francois Daoust <fd@w3.org>
 * @package AskPythia
 * @subpackage Implementation
 * @version $Revision: 1.28 $
 * @license http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231.html W3C Software Notice and License
 * @copyright Copyright (c) 2009, W3C (MIT, ERCIM, Keio)
 */

/**
 * Include the {@link InitializationException} class definition.
 */
require_once(dirname(__FILE__)."/../../interface/initializationException.php");

/**
 * Include the {@link BasicEvidence} class implementation.
 */
require_once(dirname(__FILE__).'/../basic/basicService.php');

/**
 * Include the {@link WURFLDevice} class implementation.
 */
require_once(dirname(__FILE__).'/WURFLDevice.php');


/**
 * The WURFLService class is an implementation of the DDR Simple API on top of
 * the WURFL XML database of devices.
 *
 * The WURFL XML file must have been prepared using the
 * {@link WURFLPrepareDatabase} form and class.
 *
 * The class is part of the WURFL implementation of the DDR Simple API.
 *
 * @author Sylvain Lequeux
 * @author Francois Daoust <fd@w3.org>
 * @package AskPythia
 * @subpackage Implementation
 * @link http://www.w3.org/TR/DDR-Simple-API/ Device Description Repository Simple API
 * @link http://wurfl.sourceforge.net/ WURFL
 * @version $Revision: 1.28 $
 * @license http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231.html W3C Software Notice and License
 * @copyright Copyright (c) 2009, W3C (MIT, ERCIM, Keio)
 */
class WURFLService extends BasicService {
	/**
	 * @var string The DDR Core Vocabulary namespace
	 */
	static public $CORE_VOCABULARY = 'http://www.w3.org/2008/01/ddr-core-vocabulary';
	/**
	 * @var string The Vocabulary that defines the "tableSupport" property.
	 */
	static public $WURFL_VOCABULARY = 'http://wurfl.sourceforge.net';

	/**
	 * The list of properties supported by this implementation and defined in
	 * the {@link http://www.w3.org/TR/ddr-core-vocabulary/ DDR Core Vocabulary}
	 * standard.
	 *
	 * The list is to be viewed as a tree. The first level defines the local
	 * property name of the property. The second level contains the different
	 * parameters associated with the property:
	 * - the 'aspects' parameter lists the aspects the property may apply to,
	 *   the first one in the list being the default aspect of the property.
	 * - the 'type' parameter is used to cast values appropriately when the XML
	 *   description of the device is loaded in memory. 
	 *
	 * The namespace of the properties is that of the DDR Core Vocabulary:
	 *  http://www.w3.org/2008/01/ddr-core-vocabulary
	 *  
	 * @var array(string=>array(string=>mixed)) The "tree" of properties.
	 */
	static public $CORE_PROPERTIES = array(
		'vendor' => array(
			'type' => 'string',
			'aspects' => array('device', 'webBrowser')),
		'model' => array(
			'type' => 'string',
			'aspects' => array('device', 'webBrowser')),
		//'version' => array(
		//	'type' => 'string',
		//	'aspects' => array('device', 'webBrowser')),
		'displayWidth' => array(
			'type' => 'int',
			'aspects' => array('device', 'webBrowser')),
		'displayHeight' => array(
			'type' => 'int',
			'aspects' => array('device', 'webBrowser')),
		'displayColorDepth' => array(
			'type' => 'int',
			'aspects' => array('device')),
		//'inputDevices' => array(
		//	'type' => 'array',
		//	'aspects' => array('device')),
		'markupSupport' => array(
			'type' => 'array',
			'aspects' => array('webBrowser')),
		'stylesheetSupport' => array(
			'type' => 'array',
			'aspects' => array('webBrowser')),
		'imageFormatSupport' => array(
			'type' => 'array',
			'aspects' => array('webBrowser')),
		'inputModeSupport' => array(
			'type' => 'array',
			'aspects' => array('webBrowser')),
		'cookieSupport' => array(
			'type' => 'bool',
			'aspects' => array('webBrowser')),
		'scriptSupport' => array(
			'type' => 'array',
			'aspects' => array('webBrowser')),
	);

	/**
	 * The list of properties supported by this implementation and defined in
	 * the table vocabulary.
	 *
	 * See {@link $CORE_PROPERTIES} for more details on the format of the list.
	 * 
	 * @var array(string=>array(string=>mixed)) The "tree" of properties.
	 */
	static public $WURFL_PROPERTIES = array(
		'xhtml_table_support' => array(
			'type' => 'bool',
			'aspects' => array('__NULL')),
		'is_wireless_device' => array(
			'type' => 'bool',
			'aspects' => array('__NULL'))
	);

	/**
	 * The complete list of properties supported by this implementation and
	 * represented as a tree.
	 *
	 * See {@link basicService} for more details on the format of the list.
	 *
	 * The list is initialized when {@link initializeProperties()} is called
	 * for the first time.
	 *
	 * @var array(string=>array(string=>array(string=>mixed))) The complete "tree" of properties.
	 */
	static public $SUPPORTED_PROPERTIES;

		
	/**
	 * @var string Full path to the file that contains the complete WURFL database.
	 */
	private $wurflFile;
	/**
	 * @var string Full path to the file that contains the list of families of
	 *   devices in the WURFL database along with the offset and length of the
	 *   family in the WURFL devices file. 
	 */
	private $wurflFamilies;
	/**
	 * @var string Full path to the file that contains the list of devices of
	 *   the WURFL database along with the offset and length of the device
	 *   description in the complete WURFL database. 
	 */
	private $wurflDevices;
	/**
	 * @var array(string=>WURFLDevice) The cache as User-Agent/WURFLDevice pairs.
	 */
	private $deviceCache;
	
	
	/**
	 * Initializes specific WURFL settings.
	 *
	 * The method is called automatically when {@link ServiceFactory::newService()}
	 * is used to create an instance of the WURFLService class.
	 *
	 * The only setting that may be set is the path to the WURFL set of prepared
	 * XML files. Add a "wurfl_path" key to the $props argument that points to
	 * the appropriate path.
	 *
	 * @param string $defaultVocabularyIRI the IRI of the default vocabulary.
	 * @param array(string=>string) $props See above.
	 * @exception InitializationException there was a problem during initialization.
	 */
	protected function initializeProperties($props){
		if (!isset(self::$SUPPORTED_PROPERTIES)){
			self::$SUPPORTED_PROPERTIES = array(
				self::$CORE_VOCABULARY => self::$CORE_PROPERTIES,
				self::$WURFL_VOCABULARY => self::$WURFL_PROPERTIES,
			);
		}
		
		$this->supportedProperties = self::$SUPPORTED_PROPERTIES;
		$this->deviceCache = array();

		if(!array_key_exists('wurfl_path', $props)){
			throw new InitializationException(
				'The path to the WURFL database must be set in a "wurfl_path" key.',
				InitializationException::$INITIALIZATION_ERROR);
		}
		
		$this->setWurflPath($props['wurfl_path']);
	}


	/**
	 * Builds a PropertyValues object from the given evidence.
	 * We search the device corresponding to the $evidence with
	 * this algorithm :
	 * We compare all the user agents of the content to exclude
	 * those which are not begining by the same token as the evidence
	 * user agent.
	 * Then we keep the one which is the nearer in terms of Levenstein
	 * distance.
	 *
	 * @param Evidence $evidence The evidence to use to identify the device.
	 * @return PropertyValues Known properties of the device identfied by the evidence.
	 */
	protected function getPropertyValues($evidence){
		$userAgent = $evidence->get('user-agent');
		if(!isset($userAgent)){
			throw new SystemException(
				'User-Agent not set!',
				SystemException::$ILLEGAL_ARGUMENT
			);
		}
		
		$device = $this->getDevice($userAgent);
		if(!isset($device)){
			// No device found that matches the evidence
			return new BasicPropertyValues();
		}
		else {
			return $device->getPropertyValues(); 
		}
	}

	/**
	 * Returns the implementation of the WURFL implementation of the DDR Simple API.
	 *
	 * @return string The implementation version.
	 */
	public function getImplementationVersion(){
		$cvsRevision = '$Revision: 1.28 $';
		$versionStart = strlen('$Revision: ');
		$versionEnd = strpos($cvsRevision, ' ', $versionStart);
		$version = substr($cvsRevision, $versionStart, $versionEnd - $versionStart);
		$implementation = 'W3C implementation of the DDR Simple API in PHP on top of WURFL, v' . $version;  
		return $implementation;
	}

	/**
	 * Returns the underlying version of the WURFL database being used.
	 *
	 * @return string The underlying version of the WURFL database.
	 */
	public function getDataVersion(){
		$xml = simplexml_load_file($this->wurflFile);
		$root = $xml->devices;
		return $root['version'];
	}
	
	/**
	 * Sets the path of the xml file to use.
	 *
	 * @param string $path Path that contains the prepared WURFL files.
	 */
	private function setWurflPath($path){
		$this->wurflFile     = $path . 'wurfl_patched.xml';
		$this->wurflDevices  = $path . 'wurfl_devices.xml';
		$this->wurflFamilies = $path . 'wurfl_families.xml';
		
		if(!file_exists($this->wurflFile)){
			throw new InitializationException(
				'The patched WURFL file "' . $this->wurflFile . '" does not exist.',
				InitializationException::$INITIALIZATION_ERROR);
		}
		if(!file_exists($this->wurflDevices)){
			throw new InitializationException(
				'The file "' . $this->wurflDevices . '" that should contain the list' .
				' of devices in the WURFL database does not exist.',
				InitializationException::$INITIALIZATION_ERROR);
		}
		if(!file_exists($this->wurflFamilies)){
			throw new InitializationException(
				'The file "' . $this->wurflFamilies . '" that should contain the list' .
				' of families of devices in the WURFL database does not exist.',
				InitializationException::$INITIALIZATION_ERROR);
		}
	}
	
	
	/**
	 * Returns the WURFL device that matches the given evidence.
	 * 
	 * @param Evidence $evidence The evidence to use to identify the device.
	 * @return WURFLDevice the device that best matches the evidence.
	 */
	private function getDevice($userAgent){
		$device = $this->getDeviceFromCache($userAgent);
		if(isset($device)){
			return $device;
		}
		
		$device = $this->getDeviceFromRepository($userAgent);
		$this->addDeviceToCache($userAgent, $device);
		//var_dump($device);
		return $device;
	}
	
	/**
	 * Searches the given User-Agent in the device cache, and returns the
	 * corresponding device if found.
	 *  
	 * @param string $userAgent User-Agent to search in the cache.
	 * @return WURFLDevice Device that matches the User-Agent, NULL when the
	 *   device could not be found in the cache. 
	 */
	private function getDeviceFromCache($userAgent){
		foreach($this->deviceCache as $ua=>$device){
			if($ua === $userAgent){
				return $device;
			}
		}
		return NULL;
	}
	
	/**
	 * Inserts a device in the cache to speed up later retrievals.
	 * 
	 * @param string $userAgent The User-Agent that was used to identify the device.
	 * @param WURFLDevice $device The device to add in the cache
	 */
	private function addDeviceToCache($userAgent, $device){
		// TODO: implement some cache limit in a future version.
		$this->deviceCache[$userAgent] = $device;
	}
	
	/**
	 * Searches the given User-Agent in the repository and returns the
	 * corresponding device.
	 *  
	 * @param string $userAgent The User-Agent to use to identify the device.
	 * @return WURFLDevice The device that matches the User-Agent, an empty
	 *  (but non NULL) device when the device could not be found.
	 */
	private function getDeviceFromRepository($userAgent){
		try{
			// Compute the offset of the device in the WURFL file
			$deviceOffset = $this->getDeviceFileOffset($userAgent);
			
			// Retrieve the XML description of the device 
			$handle = fopen($this->wurflFile, 'r');
			$res = fseek($handle, $deviceOffset[0]);
			if($res != 0){
				throw new SystemException(
					'The WURFL family file is corrupted.',
					SystemException::$CANNOT_PROCEED);
			}
			$content = fread($handle, $deviceOffset[1]);
			fclose($handle);
			
			// Parse the XML and initialize the WURFLDevice object
			$deviceXml = simplexml_load_string($content);
			$deviceAttributes = $deviceXml->attributes();
			
			$device = new WURFLDevice($deviceAttributes->user_agent);
			foreach($deviceXml->capability as $capa){//$devicexml => $deviceXml
				$attrs = $capa->attributes();
				$capaName = (string) $attrs->name;
				$capaValue = (string) $attrs->value;
				$capaAspect = (string) $attrs->aspect;
				
				// Note the underlying database only contains local property names
				// because a given local name only belongs to one and only one
				// vocabulary.
				if(array_key_exists($capaName, self::$WURFL_PROPERTIES)){
					$capaNamespace = self::$WURFL_VOCABULARY;
					$capaDesc = self::$WURFL_PROPERTIES[$capaName];
				}
				else{
					$capaNamespace = self::$CORE_VOCABULARY;
					$capaDesc = self::$CORE_PROPERTIES[$capaName];				
				}
				$capaType = $capaDesc['type'];
				//var_dump($capaValue);echo '<br />';
				if($capaType == 'bool'){
					if(empty($capaValue));
					$capaValue = ($capaValue == 'true');
				}
				else if($capaType == 'array'){
					$capaValue = explode('//', $capaValue);
				}
				else if (!settype($capaValue, $capaType)) {
					throw new SystemException(
						'The WURFL database is corrupted: ' . 
						'the property value "' . $capaValue . '" for local property name "' .
						$capaName . '" cannot be casted to a ' . $capaType . '.',
						SystemException::$CANNOT_PROCEED);
				}
				
				$propName = new BasicPropertyName($capaNamespace, $capaName);
				$propRef = new BasicPropertyRef($propName, $capaAspect);
				$propValue = new BasicPropertyValue($propRef, $capaValue);
				$device->getPropertyValues()->add($propValue);
			}
		}
		catch(SystemException $e){
			throw $e;
		}
		catch(Exception $e){
			throw new SystemException(
				'An internal error occurred.',
				SystemException::$CANNOT_PROCEED,
				$e);
		}
		
		return $device;
	}
	
	/**
	 * Returns the offset of the device identified by the given User-Agent
	 * in the prepared WURFL XML file.
	 *  
	 * @param string $userAgent The User-Agent that identifies the device.
	 * @return (int,int) The position and length of the device description
	 *   in the WURFL file (in bytes).
	 */
	private function getDeviceFileOffset($userAgent){
		// Compute the offset of the User-Agent's family in the token file.
		$familyOffset = $this->getDeviceFamilyFileOffset($userAgent);
		
		// Retrieve the list of devices that compose the family
		$handle = fopen($this->wurflDevices, 'r');
		$res = fseek($handle, $familyOffset[0]);
		if($res != 0){
			throw new SystemException(
				'The WURFL token file is corrupted.',
				SystemException::$CANNOT_PROCEED
			);
		}
		$content = fread($handle, $familyOffset[1]);
		fclose($handle);
		
		// Parse the list of devices that compose the family
		// and select the one that best matches the requested User-Agent
		$minLeven = PHP_INT_MAX;
		$deviceList = simplexml_load_string($content);
		foreach($deviceList->device as $currDevice){
			$currUserAgent = (string)$currDevice->attributes()->user_agent;
			$currLeven = levenshtein($currUserAgent, $userAgent);
			
			if($currLeven < $minLeven){
				// Better User-Agent found
				$minLeven = $currLeven;
				$foundUserAgent = $currUserAgent;
				$foundOffset = intval($currDevice->attributes()->offset);//$device => $currDevice
				$foundLength = intval($currDevice->attributes()->length);//$device => $currDevice
			}
		}
		
		$offset = array($foundOffset, $foundLength);
		return $offset;
	}
	
	/**
	 * Returns the offset of the family that matches the family of the given
	 * User-Agent in the family view generated from the WURFL database.
	 *  
	 * @param string $userAgent The User-Agent that identifies the device.
	 * @return (int,int) The position and length of the family (i.e. the list
	 *   of devices) that matches the family of the User-Agent (in bytes).
	 */
	private function getDeviceFamilyFileOffset($userAgent){
		// The family is the first token of the User-Agent,
		// or the first family in the file if not found.
		$deviceTokens = explode(' ', $userAgent);
		$deviceFamily = strtolower($deviceTokens[0]);
		
		$foundFamily = NULL;
		$foundOffset = 0;
		$foundLength = 0;
		
		$families = simplexml_load_file($this->wurflFamilies);
		foreach($families->token as $currToken){
			$currFamily = (string)$currToken->attributes()->name;
			if(!isset($foundFamily)){
				$foundFamily = $currFamily;
				$foundOffset = intval($currToken->attributes()->offset);//$token => $currToken
				$foundLength = intval($currToken->attributes()->length);//$token => $currToken
			}
			if($currFamily == $deviceFamily){
				// Note that for the comparison to work fine, WURFLPrepareDatabase
				// must write family names using lower-cased characters.
				$foundFamily = $currFamily;
				$foundOffset = intval($currToken->attributes()->offset);//$token => $currToken
				$foundLength = intval($currToken->attributes()->length);//$token => $currToken
				break;
			}
		}
		
		$offset = array($foundOffset, $foundLength);
		return $offset;
	}
}
?>