<?php
/**
 * This file is part of the transcoding library. It contains the
 * definition of the {@link TranscodingAction} class.
 * 
 * @author Sylvain Lequeux
 * @author Francois Daoust <fd@w3.org>
 * @package TransPythia
 * @version $Revision: 1.25 $
 * @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 SystemException} class description.
 */
require_once(dirname(__FILE__) . '/../ddrsimpleapi/interface/systemException.php');


/**
 * TranscodingAction is an abstract class for all the transcoding actions that may
 * be managed by a {@link Transcoder}.
 * 
 * A transcoding action is defined within the transcoding library as an action that
 * converts content to match the capabilities of the device that requests the
 * content. The capabilities of the device are retrieved from a DDR {@link Service}
 * based on an {@link Evidence}. See {@link apply()}.
 * 
 * The class features a generic pair/value option mechanism that may be used in
 * concrete implementations to fine-tune the behavior of the transcoding action.
 * 
 * The {@link apply()} method must be implemented in concrete subclasses. 
 * 
 * @author Sylvain Lequeux
 * @author Francois Daoust <fd@w3.org>
 * @package TransPythia
 * @version $Revision: 1.25 $
 * @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)
 */
abstract class TranscodingAction {
	/**
	 * @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';
	
	/**
 	* Default DDR property reference used to identify mobile devices.
 	*/
	static public $WURFL_MOBILE_DEVICE = 'is_wireless_device';
	static public $WURFL_DEFAULT_ASPECT = '__NULL';
	
	/**
	 * @var Service The DDR service to use to retrieve device capabilities.
	 */
	private $service;
	
	/**
	 * @var array(string=>mixed) A generic set of options that may be used
	 *      in concrete subclasses to store custom user settings.
	 *      Option values may be of any type. Transcoding actions should
	 *      ensure options are correctly set before accessing them through
	 *      calls to {@link initOption()} and {@link initProperty()}.
	 */
	private $options;
	
	/**
	 * @var PropertyValues The list of properties of the identified device.
	 */
	private $propValues;
	
	
	/**
	 * Creates an instance of the transcoding action associated with the given
	 * DDR service.
	 * 
	 * @param Service $service The DDR service to use to retrieve the
	 *                         capabilities of the requesting device.
	 * @return A new transcoding action.
	 * @exception SystemException The given service is invalid.
	 */
	public function __construct($service){
		if(!isset($service)){
			throw new SystemException(
				'The DDR service cannot be null.',
				SystemException::$ILLEGAL_ARGUMENT);
		}
		if(!($service instanceof Service)){
			throw new SystemException(
				'The DDR service does not implement the Service interface.',
				SystemException::$ILLEGAL_ARGUMENT);
		}
		
		$this->service = $service;
		$this->options = array('properties'=>array());
	}
	
	
	/**
	 * Applies the transcoding action to the given content, using the
	 * capabilities of the device identified by the given evidence.
	 * 
	 * The method must be implemented in concrete subclasses.
	 * 
	 * @param string $content The content to be modified.
	 * @param Evidence $evidence The evidence that identifies the requesting device.
	 * @return string The transcoded content.
	 * @exception SystemException The evidence is not valid.
	 */
	public abstract function apply($content, $evidence);
	
	
	/**
	 * Sets a transcoding option.
	 * 
	 * Options are just a generic pair/value settings mechanisms that may
	 * be used in concrete subclasses. In particular, options are not used
	 * within this class.
	 * 
	 * @param string $optionName The name of the option.
	 * @param mixed $optionValue The value of the option.
	 */
	public function setOption($optionName, $optionValue){
		$this->options[$optionName] = $optionValue;
	}
	
	/**
	 * Retrieves a transcoding option.
	 * 
	 * Options are just a generic pair/value settings mechanisms that may
	 * be used in concrete subclasses. In particular, options are not used
	 * within this class.
	 * 
	 * @param string $optionName The name of the option.
	 * @return mixed The option value.
	 * @exception SystemException The option does not exist.
	 */
	public function getOption($optionName){
		if(!array_key_exists($optionName, $this->options)){
			throw new SystemException(
				'The option does not exist.',
				SystemException::$ILLEGAL_ARGUMENT);
		}
		return $this->options[$optionName];
	}
	
	/**
	 * Initializes the list of capabilities of the device identified by the
	 * given Evidence.
	 * 
	 * This method must be called prior to calling {@link getProperty()}.
	 * 
	 * Please note that concrete subclasses may choose to interact with the
	 * DDR service directly instead of relying on these two methods. In
	 * particular, the method is not used within this class.
	 * 
	 * @param Evidence $evidence The evidence that identifies the requesting device.
	 * @exception SystemException The evidence is not valid.
	 */
	protected function initPropertyValues($evidence){
		if(!isset($evidence)){
			throw new SystemException(
				'The evidence cannot be null.',
				SystemException::$ILLEGAL_ARGUMENT);
		}
		if(!($evidence instanceof Evidence)){
			throw new SystemException(
				'The evidence does not implement the Evidence interface.',
				SystemException::$ILLEGAL_ARGUMENT);
		}
		
		$this->propValues = $this->service->getPropertyValuesE($evidence);
	} 
	
	/**
	 * Returns the property value of the device identified by a prior call to
	 * {@link initPropertyValues()} that matches the given local property name,
	 * vocabulary IRI and aspect name.
	 * 
	 * Please note that the underlying DDR Simple API implementation may throw
	 * exceptions if the arguments are not valid. 
	 * 
	 * @param string $name The local property name
	 * @param string $namespace The vocabulary IRI the property belongs to
	 * @param string $aspectName The property aspect name
	 * @return PropertyValue The property value for the device identified through
	 *                       a prior call to {@link initPropertyValues()}.
	 * @exception SystemException The capabilities have not been properly initialized. 
	 */
	protected function getPropertyValue($name, $namespace, $aspectName){
		if(!isset($this->propValues)){
			throw new SystemException(
				'No device identified. initPropertyValues has probably not been called.',
				SystemException::$CANNOT_PROCEED);
		}
		
		$propName = $this->service->newPropertyNameSS($name, $namespace);
		$propRef = $this->service->newPropertyRefPnS($propName, $aspectName);
		return $this->propValues->getValue($propRef);
	}
	
	/**
	 * Returns the property value of the device identified by a prior call to
	 * {@link initPropertyValues()} that matches the given local property name,
	 * vocabulary IRI and aspect name.
	 * 
	 * Please note that the underlying DDR Simple API implementation may throw
	 * exceptions if the arguments are not valid. 
	 * 
	 * @param PropertyRef $propRef The reference of the property value to retrieve
	 * @return PropertyValue The property value for the device identified through
	 *                       a prior call to {@link initPropertyValues()}.
	 * @exception SystemException The capabilities have not been properly initialized. 
	 */
	protected function getPropertyValuePr($propRef){
		if(!isset($this->propValues)){
			throw new SystemException(
				'No device identified. initPropertyValues has probably not been called.',
				SystemException::$CANNOT_PROCEED);
		}
		
		return $this->propValues->getValue($propRef);
	}
	
	/**
	 * Checks the
	 * Check if default values (name, namespace and aspect) are set 
	 * for a property. If not, then use the default value.
	 * 
	 * @param string $name The name of the property to initialize
	 */
	protected function initProperty($name, $defaultName, $defaultNamespace, $defaultAspect){
		if(!isset($name)){
			throw new SystemException(
					'The name of the property to check is not set',
					SystemException::$ILLEGAL_ARGUMENT
				);
		}
		if(!isset($defaultName)){
			throw new SystemException(
					'There is no default name for '.$name,
					SystemException::$ILLEGAL_ARGUMENT
				);
		}
		if(!isset($defaultNamespace)){
			throw new SystemException(
					'There is no default namespace for '.$name,
					SystemException::$ILLEGAL_ARGUMENT
				);
		}
		if(!isset($defaultAspect)){
			throw new SystemException(
					'There is no default aspect for '.$name,
					SystemException::$ILLEGAL_ARGUMENT
				);
		}
		
		if(!array_key_exists($name, $this->options)){
			$propName = $this->service->newPropertyNameSS($defaultName, $defaultNamespace);
			$propRef = $this->service->newPropertyRefPnS($propName, $defaultAspect);
			$this->options[$name] = $propRef;
		}
	}
	
	/**
	 * Ensures that the given option is defined in the list of options, and is of
	 * the appropriate type. Sets the option to the given default value if the
	 * option is not in the list of options yet.
	 * 
	 * @param string $name The option's name.
	 * @param mixed $type The expected type of the option value.
	 * @param mixed $default The option's default value if not already set.
	 * @exception SystemException Option value found with an invalid type.
	 */
	protected function initOption($name, $type, $default){
		if(!array_key_exists($name, $this->options)){
			$this->options[$name] = $default;
		}
		$this->checkType($name, $type);
	}
	
	/**
	 * Throws an exception if the option's value of the requested option's name
	 * does not match the expected type.
	 * 
	 * @param string $name The option's name.
	 * @param mixed $type The expected type of the option value.
	 * @exception SystemException Option value found with an invalid type.
	 */
	private function checkType($name, $type){
		$var = $this->options[$name];
		$type_lower_case = strtolower($type);
		switch($type_lower_case){
			case 'int'    : $valid = is_int($var);	 		  break;
			case 'double' : $valid = is_double($var); 		  break;
			case 'float'  : $valid = is_float($var); 		  break;
			case 'long'   : $valid = is_long($var); 		  break;
			case 'array'  : $valid = is_array($var); 		  break;
			case 'string' : $valid = is_string($var); 		  break;
			case 'bool'   : $valid = is_bool($var); 		  break;
			default 	  : $valid = ($var instanceof $type); break;
		}
		if(!$valid){
			throw new SystemException(
					'Error while checking option ' . $name . '. Wrong type ' . $type,
					SystemException::$ILLEGAL_ARGUMENT
				);
		}
	}
	
	
	/**
	 * Converts the given URI to a local file URI, when possible.
	 *  
	 * @param $uri string The URI to map. May be absolute or relative.
	 * @param $baseUri string The base URI to use to resolve the URI to map.
	 * @param $mappings array(string=>string) List of mappings between a root URI
	 *   and a root folder. The list is tried in the order in which it appears.
	 *   First possible replacement gets used.  
	 * @return string The file URI is mapping can be done, NULL otherwise.
	 */
	protected function mapUriToFile($uri, $baseUri, $mappings) {
		// Convert the URI to an absolute URI
		$absoluteUri = $this->resolveUri($baseUri, $uri);

		// Map the absolute URI to the filesystem if possible
		$fileName = $this->mapAbsoluteUriToFile(
			$absoluteUri,
			$mappings);
		return $fileName;
	}
	
	
	/**
	 * Converts the given filename to a relative or absolute URI when possible.
	 * 
	 * The relative form, where relative is to the provided base URI, is
	 * returned whenever possible. An absolute URI is returned if not. 
	 *  
	 * @param $fileName string The absolute full path to the file to map.
	 * @param $baseUri string The base URI to use to compute the returned relative
	 *   form. An absolute URI is returned when NULL or empty.  
	 * @param $mappings array(string=>string) List of mappings between a root URI
	 *   and a root folder. The list is tried in the order in which it appears.
	 *   First possible replacement gets used.  
	 * @return string The relative/absolute mapped URI if possible, NULL otherwise.
	 */
	protected function mapFileToUri($fileName, $baseUri, $mappings) {
		// Convert the filename to an absolute URI
		$absoluteUri = $this->mapFileToAbsoluteUri(
			$fileName,
			$mappings);
		
		// Convert the absolute URI to a relative one, if possible.
		$uri = $this->getRelativeUri($baseUri, $absoluteUri);
		return $uri;
	}
	
	
	/**
	 * Converts the given absolute URI to a local file URI, when possible.
	 *  
	 * @param $uri string The URI to map. May be absolute or relative.
	 * @param $mappings array(string=>string) List of mappings between a root URI
	 *   and a root folder. The list is tried in the order in which it appears.
	 *   First possible replacement gets used. 
	 * @return string the mapped filename, in absolute form. 
	 */
	protected function mapAbsoluteUriToFile($uri, $mappings) {
		foreach ($mappings as $rootUri=>$rootFolder) {
			if ($rootUri == "") {
				return $rootFolder . $uri;
			}
			else if (strstr($uri, $rootUri)) {
				return str_replace($rootUri, $rootFolder, $uri);			
			}
		}
		return NULL;
	}
	
	
	/**
	 * Converts the given absolute file name to an absolute URI, when possible.
	 *  
	 * @param $fileName string The absolute filename to map.
	 * @param $mappings array(string=>string) List of mappings between a root URI
	 *   and a root folder. The list is tried in the order in which it appears.
	 *   First possible replacement gets used. 
	 * @return string the mapped URI, in absolute form. 
	 */
	protected function mapFileToAbsoluteUri($fileName, $mappings) {
		foreach ($mappings as $rootUri=>$rootFolder) {
			if ($rootFolder == "") {
				return $rootUri . $fileName;
			}
			else if (strstr($fileName, $rootFolder)) {
				return str_replace($rootFolder, $rootUri, $fileName);
			}
		}

		return NULL;
	}
	
	
	/**
	 * Resolves a URI against a base URI
	 * 
	 * The code is based on code provided by:
	 *  alistair at 21degrees dot com dot au
	 * in the comments of
	 *  http://fr2.php.net/manual/fr/function.parse-url.php
	 * and patched by:
	 *  adrian-php at sixfingeredman dot net
	 * 
	 * This implementation more or less follows the "5.2 Resolving Relative
	 * References to Absolute Form" section of RFC 2395.
	 * 
	 * @param $base The base URI. Must be an absolute URI.
	 * @param $url The URI to resolve. The URI may be an absolute or a
	 *    relative URI.
	 * @return string The resolved absolute URI
	 */
	protected function resolveUri($base, $uri) {
        if (!strlen($base)) {
        	return $uri;
        }
        // Step 2: Reference to current document if empty
        if (!strlen($uri)) {
        	return $base;
        }
        
        // Step 3: Absolute URI case
        if (preg_match('!^[a-z]+:!i', $uri)) {
        	return $uri;
        }
        
        // Split base URI in components 
        $base = parse_url($base);
        
        // If the provided URI is a fragment reference, simply replace the
        // fragment part of the base URI by this URI and return. 
        if ($uri{0} == "#") {
                // Step 2 (fragment)
                $base['fragment'] = substr($uri, 1);
                return $this->unparse_url($base);
        }
        
        // Fragment and query components are not inherited from the base URI.
        unset($base['fragment']);
        unset($base['query']);
        
        
        if (substr($uri, 0, 2) == "//") {
        	// Step 4: authority component defined
            return $this->unparse_url(array(
            	'scheme'=>$base['scheme'],
                'path'=>$uri,
            ));
        }
        else if ($uri{0} == "/") {
            // Step 5: absolute path
            $base['path'] = $uri;
        }
        else {
        	// Step 6: relative path
            $path = explode('/', trim($base['path'], '/'));
            $url_path = explode('/', $uri);
                
            // Step 6a: drop file from base
            if (substr_compare($base['path'], "/", strlen($base['path']) - 1, 1) != 0) {
            	array_pop($path);
            }
                
            // Step 6b, 6c, 6e: append url while removing "." and ".." from
            // the directory portion
            $end = array_pop($url_path);
            foreach ($url_path as $segment) {
            	if ($segment == '.') {
            		// skip
            	}
            	else if ($segment == '..' && $path && $path[sizeof($path)-1] != '..') {
            		array_pop($path);
            	}
            	else {
            		$path[] = $segment;
            	}
           	}
           	
            // Step 6d, 6f: remove "." and ".." from file portion
            if ($end == '.') {
            }
            else if ($end == '..' && $path && $path[sizeof($path)-1] != '..') {
            	$path[sizeof($path)-1] = '';
            }
            else {
            	$path[] = $end;
            }
            
            // Step 6h
            $base['path'] = join('/', $path);
        }
        
        // Step 7
        return $this->unparse_url($base);
	}
	
	
	/**
	 * Converts an absolute URI to a URI relative to the given base URI, when
	 * possible.
	 * 
	 * @param $base The base URI. Must be an absolute URI.
	 * @param $url The absolute URI to convert.
	 * @return string The relative URI when possible,
	 *   the original absolute URI otherwise.
	 */
	protected function getRelativeUri($base, $absoluteUri) {
		if (!$base || ($base == "")) {
			return $absoluteUri;
		}
		else {
			// Split base URI in components 
        	$base = parse_url($base);
        	
        	// Step 6a: drop file from base
			$basePath = explode('/', $base['path']);
            if (substr_compare($base['path'], '/', strlen($base['path']) - 1, 1) != 0) {
            	array_pop($basePath);
            }
            
            $base['path'] = join('/', $basePath);
            
            // Fragment and query components are not inherited from the base URI.
        	unset($base['fragment']);
        	unset($base['query']);
            
            $base = $this->unparse_url($base);
            
			if (strpos($absoluteUri, $base) == 0) {
				return str_replace($base, "", $absoluteUri);
			}
			else {
				return $absoluteUri;
			}
		}
	}
	
	/**
	 * Converts a URI previously splitted through a call to parse_url back
	 * to an absolute form.
	 * 
	 * @param array(string) $parsed The parsed URI
	 * @return string The URI represented by the parsed entity
	 */
	protected function unparse_url($parsed) {
	    if (!is_array($parsed)) {
	        return false;
	    }
	
	    $uri = isset($parsed['scheme']) ? $parsed['scheme'].':'.((strtolower($parsed['scheme']) == 'mailto') ? '' : '//') : '';
	    $uri .= isset($parsed['user']) ? $parsed['user'].(isset($parsed['pass']) ? ':'.$parsed['pass'] : '').'@' : '';
	    $uri .= isset($parsed['host']) ? $parsed['host'] : '';
	    $uri .= isset($parsed['port']) ? ':'.$parsed['port'] : '';
	
	    if (isset($parsed['path'])) {
	        $uri .= (substr($parsed['path'], 0, 1) == '/') ?
	            $parsed['path'] : ((!empty($uri) ? '/' : '' ) . $parsed['path']);
	    }
	
	    $uri .= isset($parsed['query']) ? '?'.$parsed['query'] : '';
	    $uri .= isset($parsed['fragment']) ? '#'.$parsed['fragment'] : '';
	
	    return $uri;
	}
	
	
	/**
	 * Retrieves the first block found in the string that matches the given
	 * list.
	 * 
	 * The method requires the blocks being looked upon to be properly nested.
	 * 
	 * @param string $blocks The list of tag names to look for defined in a
	 *   "RegExp way", e.g.: 'p|div|table'
	 * @param string $string The string to search
	 * @param int $offset Position where search should begin in the string
	 * @return array<mixed> An array that contains information about the
	 *   first matched block, NULL when no block was found. Array structure:
	 *    0: The extracted block
	 *    1: The matched tag name
	 *    2: The block's starting offset in the string
	 *    3: The block's starting tag
	 *    4: The block's ending offset in the string
	 *    5: The block's ending tag (may be an empty string if empty tag or if
	 *    ending tag was not found)
	 */
	protected function getNextBlock($blocks, $string, $offset) {
		$startMatch = NULL;
		
		$matchFound = preg_match('/\<(' . $blocks . ')([ \t\r\n].*)?(\/?)\>/Usi',
			$string, $startMatch, PREG_OFFSET_CAPTURE, $offset);
		
		$res = NULL;
		if ($matchFound) {
			$startOffset = $startMatch[0][1];
			$startTagLength = strlen($startMatch[0][0]);
			$tagName = strtolower($startMatch[1][0]);
			if ($startMatch[3][0]) {
				$emptyTag = true;
			}
			else {
				$emptyTag = false;
			}
			
			// The block might support nesting, looking for the end of the block
			// involves counting...
			$endOffset = -1;
			$endTagLength = 0;
			if ($emptyTag) {
				// Empty tag (e.g. <hr/>)
				$endOffset = $startOffset + $startTagLength;
			}
			else {
				$openContainers = 1;
				$closingTagRegExp = '/\<\/?' . $tagName . '[ \t\r\n\/\>]/Usi';
				preg_match_all($closingTagRegExp, $string,
					$endMatches, PREG_OFFSET_CAPTURE,
					$startOffset + 1);
				foreach ($endMatches[0] as $endMatch) {
					if (substr_compare($endMatch[0], "</", 0, 2) == 0) {
						$openContainers -= 1;
						if ($openContainers == 0) {
							$endTagLength = strlen($endMatch[0]);
							$endOffset = $endMatch[1] + $endTagLength;
							break;
						}
					}
					else {
						$openContainers += 1;
					}
				}
			}
				
			if ($endOffset == -1) {
				// Block is not valid XML, let's consider there's one and only one block in there
				$endOffset = strlen($string) - 1;
			}
			
			$res = array();
			
			$res[0] = substr($string, $startOffset, $endOffset - $startOffset);
			$res[1] = $tagName;
			$res[2] = $startOffset;
			$res[3] = substr($string, $startOffset, $startTagLength);
			$res[4] = $endOffset;
			$res[5] = substr($string, $endOffset - $endTagLength, $endTagLength); 
		}
		
		return $res;
	}
}

?>