<?php

/**
 * Generic Server-Side Google Analytics PHP Client
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License (LGPL) as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
 * 
 * Google Analytics is a registered trademark of Google Inc.
 * 
 * @link      http://code.google.com/p/php-ga
 * 
 * @license   http://www.gnu.org/licenses/lgpl.html
 * @author    Thomas Bachem <tb@unitedprototype.com>
 * @copyright Copyright (c) 2010 United Prototype GmbH (http://unitedprototype.com)
 */

namespace UnitedPrototype\GoogleAnalytics\Internals;

/**
 * This is nearly a 1:1 PHP port of the gaforflash X10 class code.
 * 
 * @link http://code.google.com/p/gaforflash/source/browse/trunk/src/com/google/analytics/data/X10.as
 */
class X10 {
	
	/**
	 * @var array
	 */
	protected $projectData = array();
	
	
	/**
	 * @var string
	 */
	protected $KEY = 'k';
	
	/**
	 * @var string
	 */
	protected $VALUE = 'v';
	
	/**
	 * @var array
	 */
	protected $SET = array('k', 'v');
	
	/**
	 * Opening delimiter for wrapping a set of values belonging to the same type.
	 * @var string
	 */
	protected $DELIM_BEGIN = '(';
	
	/**
	 * Closing delimiter for wrapping a set of values belonging to the same type.
	 * @var string
	 */
	protected $DELIM_END   = ')';
	
	/**
	 * Delimiter between two consecutive num/value pairs.
	 * @var string
	 */
	protected $DELIM_SET = '*';
	
	/**
	 * Delimiter between a num and its corresponding value.
	 * @var string
	 */
	protected $DELIM_NUM_VALUE = '!';
	
	/**
	 * Mapping of escapable characters to their escaped forms.
	 * 
	 * @var array
	 */
	protected $ESCAPE_CHAR_MAP = array(
		"'" => "'0",
		')' => "'1",
		'*' => "'2",
		'!' => "'3",
	);
	
	/**
	 * @var int
	 */
	protected $MINIMUM = 1;
	
	
	/**
	 * @const int
	 */
	const OBJECT_KEY_NUM  = 1;
	/**
	 * @const int
	 */
	const TYPE_KEY_NUM    = 2;
	/**
	 * @const int
	 */
	const LABEL_KEY_NUM   = 3;
	/**
	 * @const int
	 */
	const VALUE_VALUE_NUM = 1;
	
	
	/**
	 * @param int $projectId
	 * @return bool
	 */
	protected function hasProject($projectId) {
		return isset($this->projectData[$projectId]);
	}
	
	/**
	 * @param int $projectId
	 * @param int $num
	 * @param mixed $value
	 */
	public function setKey($projectId, $num, $value) {
		$this->setInternal($projectId, $this->KEY, $num, $value);
	}
	
	/**
	 * @param int $projectId
	 * @param int $num
	 * @return mixed
	 */
	public function getKey($projectId, $num) {
		return $this->getInternal($projectId, $this->KEY, $num);
	}
	
	/**
	 * @param int $projectId
	 */
	public function clearKey($projectId) {
		$this->clearInternal($projectId, $this->KEY);
	}
	
	/**
	 * @param int $projectId
	 * @param int $num
	 * @param mixed $value
	 */
	public function setValue($projectId, $num, $value) {
		$this->setInternal($projectId, $this->VALUE, $num, $value);
	}
	
	/**
	 * @param int $projectId
	 * @param int $num
	 * @return mixed
	 */
	public function getValue($projectId, $num) {
		return $this->getInternal($projectId, $this->VALUE, $num);
	}
	
	/**
	 * @param int $projectId
	 */
	public function clearValue($projectId) {
		$this->clearInternal($projectId, $this->VALUE);
	}
	
	/**
	 * Shared internal implementation for setting an X10 data type.
	 * 
	 * @param int $projectId
	 * @param string $type
	 * @param int $num
	 * @param mixed $value
	 */
	protected function setInternal($projectId, $type, $num, $value) {
		if(!isset($this->projectData[$projectId])) {
			$this->projectData[$projectId] = array();
		}
		if(!isset($this->projectData[$projectId][$type])) {
			$this->projectData[$projectId][$type] = array();
		}
		
		$this->projectData[$projectId][$type][$num] = $value;
	}
	
	/**
	 * Shared internal implementation for getting an X10 data type.
	 * 
	 * @param int $projectId
	 * @param string $type
	 * @param int $num
	 * @return mixed
	 */
	protected function getInternal($projectId, $type, $num) {
		if(isset($this->projectData[$projectId][$type][$num])) {
			return $this->projectData[$projectId][$type][$num];
		} else {
			return null;
		}
	}
	
	/**
	 * Shared internal implementation for clearing all X10 data of a type from a
	 * certain project.
	 * 
	 * @param int $projectId
	 * @param string $type
	 */
	protected function clearInternal($projectId, $type) {
		if(isset($this->projectData[$projectId]) && isset($this->projectData[$projectId][$type])) {
			unset($this->projectData[$projectId][$type]);
		}
	}
	
	/**
	 * Escape X10 string values to remove ambiguity for special characters.
	 *
	 * @see X10::$escapeCharMap
	 * @param string $value
	 * @return string
	 */
	protected function escapeExtensibleValue($value) {
		$result = '';
		
		$value = (string)$value;
		$length = strlen($value);
		for($i = 0; $i < $length; $i++) {
			$char = $value[$i];
			
			if(isset($this->ESCAPE_CHAR_MAP[$char])) {
				$result .= $this->ESCAPE_CHAR_MAP[$char];
			} else {
				$result .= $char;
			}
		}
		
		return $result;
	}
	
	/**
	 * Given a data array for a certain type, render its string encoding.
	 * 
	 * @param array $data
	 * @return string
	 */
	protected function renderDataType(array $data) {
		$result = array();
		
		$lastI = 0;
		ksort($data, SORT_NUMERIC);
		foreach($data as $i => $entry) {
			if(isset($entry)) {
				$str = '';
				
				// Check if we need to append the number. If the last number was
				// outputted, or if this is the assumed minimum, then we don't.
				if($i != $this->MINIMUM && $i - 1 != $lastI) {
					$str .= $i;
					$str .= $this->DELIM_NUM_VALUE;
				}
	
				$str .= $this->escapeExtensibleValue($entry);
				$result[] = $str;
			}
			
			$lastI = $i;
		}
		
		return $this->DELIM_BEGIN . implode($this->DELIM_SET, $result) . $this->DELIM_END;
	}
	
	/**
	 * Given a project array, render its string encoding.
	 * 
	 * @param array $project
	 * @return string
	 */
	protected function renderProject(array $project) {
		$result = '';
	
		// Do we need to output the type string? As an optimization we do not
		// output the type string if it's the first type, or if the previous
		// type was present.
		$needTypeQualifier = false;
		
		$length = count($this->SET);
		for($i = 0; $i < $length; $i++) {
			if(isset($project[$this->SET[$i]])) {
				$data = $project[$this->SET[$i]];
				
				if($needTypeQualifier) {
					$result .= $this->SET[$i];
				}
				$result .= $this->renderDataType($data);
				$needTypeQualifier = false;
			} else {
				$needTypeQualifier = true;
			}
		}
		
		return $result;
	}
	
	/**
	 * Generates the URL parameter string for the current internal extensible data state.
	 * 
	 * @return string
	 */
	public function renderUrlString() {
		$result = '';
		
		foreach($this->projectData as $projectId => $project) {
			$result .= $projectId . $this->renderProject($project);
		}
		
		return $result;
	}
	
}

?>