<?php defined('SYSPATH') or die('No direct access allowed.');

/**
 * This class provides access to Google's Chart API
 *
 * @package    lnApp
 * @subpackage GoogleChart
 * @category   Helper
 * @author     Deon George
 * @copyright  (c) 2010 Open Source Billing
 * @license    http://dev.osbill.net/license.html
 */
class GoogleChart implements Iterator,Countable {
	// Chart URL
	private $url = 'http://chart.apis.google.com/chart';
	// Hold our plot data
	private $plotdata = array();
	private $chartdata = array();
	// Number of Series to plot
	private $numseries = 0;
	// The type of chart we'll plot
	private $chart_type = '';
	// Default chart tytle.
	private $chart_title = '';
	// Default chart size.
	private $chart_size = '700x200';
	// Colors to use for series
	private $series_colors = array('AAACCC','E0E0E0','CCC888','EEEBBB','666CCC','888888');
	// Data encoding type to use
	private $data_encode = 's';
	private $div_mode = FALSE;

	// Implementation Methods
	public function rewind() {
		reset($this->plotdata);
	}
	public function current() {
		return current($this->plotdata);
	}
	public function key() {
		return key($this->plotdata);
	}
	public function next() {
		return next($this->plotdata);
	}
	public function valid() {
		return key($this->plotdata) ? TRUE : FALSE;
	}
	public function count() {
		return count($this->plotdata);
	}

	// Chart Types
	private $cht = array(
		// Line
//		'line'                 => 'lc',
//		'sparkline'            => 'ls',
//		'line_xy'              => 'lxy',
		// Bar
//		'horizontal_bar'       => 'bhs',
		'vertical_bar'         => 'bvs',
//		'horizontal_bar_grp'   => 'bhg',
//		'vertical_bar_grp'     => 'bvg',
		// Pie
//		'pie'                  => 'p',
//		'pie_3d'               => 'p3',
//		'pie_concentric'       => 'pc',
		// Venn
//		'venn'                 => 'v',
		// Scatter
//		'scatter'              => 's',
		// Radar
//		'radar'                => 'r',
//		'radar_fill'           => 'rs',
		// Maps
//		'map'                  => 't',
		// Google-o-meter
//		'google_o_meter'       => 'gom',
		// QR
//		'qr'                   => 'qr',
		);

	public function __construct($type) {
		if (empty($this->cht[$type]))
			throw new Kohana_Exception('Unknown chart type :type for :class',array(':type'=>$type,':class'=>get_class($this)));

		$this->chart_type = $this->cht[$type];
	}

	public function __set($key,$value) {
		switch ($key) {
			case 'div':
				$this->div_mode = $value;
				break;

			case 'encode':
				// Encoding options are t=none,s=simple,e=extend
				if (! in_array($value,array('t','s','e')))
					throw new Kohana_Exception('Unknown encoding type :type',array(':type'=>$value));

				$this->data_encode = $value;
				break;

			case 'title':
				$this->chart_title = $value;
				break;

			default:
				throw new Kohana_Exception('Unknown variable :key (:value) for :class',array(':key'=>$key,':value'=>$value,':class'=>get_class($this)));
		}
	}

	public function __toString() {
		try {
			return $this->render();
		}
		catch (Exception $e) {
			echo Debug::vars($e);
		}
	}

	/**
	 * Return an instance of this class
	 *
	 * @return GoogleChart
	 */
	public static function factory($type) {
		return new GoogleChart($type);
	}

	public function series($data) {
		// Quick Sanity check that we have the right indexes
		foreach (array('data','axis') as $k)
			if (! isset($data[$k]))
				throw new Kohana_Exception('Missing key :key',array(':key'=>$k));

		// Quick check that we have the right types
		if (! in_array($data['axis'],array('x','r')))
				throw new Kohana_Exception('Unknown data type :type',array(':type'=>$data['axis']));

		if (is_array($data['data'])) {
			foreach ($data['data'] as $title => $values) {
				$this->numseries++;

				$this->chartdata['legend'][$this->numseries] = $title;
				$this->chartdata['axis'][$data['axis']][] = $this->numseries;

				if (is_array($values))
					foreach ($values as $k => $v)
						$this->plotdata[$k][$data['axis']][$this->numseries] = $v;
				else
					throw new Kohana_Exception('Series data needs to be an array');
			}

		} else {
			throw new Kohana_Exception('Series data needs to be an array');
		}
	}

	private function seriescolors() {
		$return = array();

		$i = 0;
		for ($j=0;$j<$this->numseries;$j++) {
			array_push($return,$this->series_colors[$i++]);

			// Reset $i if we exceed our available colors
			if ($i > count($this->series_colors))
				$i = 0;
		}

		return $return;
	}

	public function series_colors() {
		return implode(',',$this->seriescolors());
	}

	public function series_chm() {
		if (! isset($this->chartdata['axis']['r']))
			return '';

		$return = array();
		$c = $this->seriescolors();
		foreach ($this->chartdata['axis']['r'] as $v)
			array_push($return,sprintf('%s,%s,%s,%s,%s,%s','D',$c[$v-1],$v-1,0,2,2));// @todo 'D,0,2,2' May need to be configurable

		return implode('|',$return);
	}

	public function series_x_labels() {
		// @todo Why the 0:?
		return '0:|'.implode('|',array_keys($this->plotdata));
	}

	public function series_scaling() {
		$return = array();

		foreach ($this->chartdata['max'] as $k => $v)
			array_push($return,sprintf('%s,%s',empty($this->chartdata['min']) ? 0 : $this->chartdata['min'],$v));

		return implode(',',$return);
	}

	public function series_scale() {
		$return = array();

		$i = 1;
		// @todo need to add min values
		foreach ($this->chartdata['max'] as $k => $v)
			array_push($return,sprintf('%s,0,%s,0',$i++,$v));

		return implode('|',$return);
	}

	public function series_legend() {
		return implode('|',$this->chartdata['legend']);
	}

	public function series_data() {
		$return = $sreturn = $invs = array();

		$sd = $max = array();
		foreach ($this->plotdata as $label => $seriesdata)
			foreach ($seriesdata as $type => $data) {
				foreach ($data as $key => $value) {
					$sd[$key][$label] = $value;
					$max[$key] = $this->chartdata['max'][$type];
					$invs[$type][$key] = TRUE;
				}
			}

		foreach ($sd as $k => $v)
			array_push($sreturn,$this->encode($v,$max[$k]));

		// @todo This might need some more work, when used with different graph types.
		if (count($invs) > 1)
			$prefix = sprintf('%s:',count($invs['x']));
		else
			$prefix = ':';

		// If encoding is text, we need to separate the series with a |
		if ($this->data_encode == 't')
			return $prefix.implode('|',$sreturn);
		else
			return $prefix.implode(',',$sreturn);
	}

	private function render() {
		$output = '';

		// Testing
		if ($this->div_mode) {
			$output .= '<div id="GoogleChart">GoogleChart</div>';
			$output .= '<div id="showgc" style="display: inline;">';
		}

		// Render
		$output .= sprintf('<img src="%s?%s" alt="%s">',$this->url,http_build_query($this->build()),_('Traffic Summary'));
//		$output .= Debug::vars($this->build());
//		$output .= Debug::vars($this->chartdata);
//		$output .= Debug::vars($this->plotdata);

		// Toggle the display of the chart.
		// @todo This JS should be placed elsewhere for HTML validation
		if ($this->div_mode) {
			$output .= '</div>';
			$output .= '<script type="text/javascript">$("#GoogleChart").click(function() {$(\'#showgc\').toggle();});</script>';
		}

		return $output;
	}

	public function html_table($vertical=TRUE,$class=array()) {
		if (! count($this))
			return sprintf('<table><tr><td>%s</td></tr></table>',_('No Data'));

		$output = sprintf('<table %s>',
			isset($class['table']) ? $class['table'] : 'class="google-data-table"');

		if ($vertical) {
			$output .= '<tr>';
			$output .= '<td>&nbsp;</td>';
			foreach ($this->chartdata['axis'] as $axis => $adetails)
				foreach ($adetails as $k)
					$output .= sprintf('<th style="text-align: right;">%s</th>',$this->chartdata['legend'][$k]);
			$output .= '</tr>';

			foreach ($this as $k => $v) {
				$output .= '<tr>';
				$output .= sprintf('<th style="text-align: right;">%s</th>',$k);
				foreach ($this->chartdata['axis'] as $axis => $adetails)
					foreach ($adetails as $k)
						$output .= sprintf('<td style="text-align: right;">%s</td>',$v[$axis][$k]);
				$output .= '</tr>';
			}

		// Horizontal table
		} else {
			$output .= '<tr>';
			$output .= '<td>&nbsp;</td>';
			foreach ($this as $k => $v)
				$output .= sprintf('<th style="text-align: right;">%s</th>',$k);
			$output .= '</tr>';

			foreach ($this->chartdata['axis'] as $axis => $adetails)
				foreach ($adetails as $id) {
					$output .= '<tr>';

					$output .= sprintf('<th style="text-align: right;">%s</th>',$this->chartdata['legend'][$id]);

					foreach ($this as $k => $v)
						$output .= sprintf('<td style="text-align: right;">%s</td>',$v[$axis][$id]);

					$output .= '</tr>';
				}
		}

		$output .= '</table>';

		return $output;
	}

	/**
	 * Some pre-render processing
	 */
	private function process() {
		$max = array();

		// Work out our maximum number for each series.
		foreach ($this->plotdata as $label => $seriesdata) {

			foreach ($seriesdata as $type => $data) {
				$c = 0;
				foreach ($data as $value)
					$c += $value;

				// Scale up our max, so we get some extra space at the top of our chart.
				// @todo This should be configurable.
				$c *= 1.1;

				if (! isset($this->chartdata['max'][$type]))
					$this->chartdata['max'][$type] = $c;
				else
					$this->chartdata['max'][$type] = ($c > $this->chartdata['max'][$type]) ? $c : $this->chartdata['max'][$type];
			}
		}
	}

	/**
	 * Build the chart
	 */
	private function build() {
		if ($this->plotdata) {
			$this->process();

			return array(
				'chf'=>'bg,s,FFFFFF00',
				'cht'=>$this->chart_type,
				'chs'=>$this->chart_size,
				'chtt'=>$this->chart_title,
				'chbh'=>'a', // @todo This might need to be calculated, valid options (a,r);
				'chg'=>'7.7,12.5,1,5', // @todo This should be calculated
				'chco'=>$this->series_colors(),
				'chdl'=>$this->series_legend(),
				'chd'=>$this->data_encode.$this->series_data(),
//				'chds'=>$this->series_scaling(),
				'chm'=>$this->series_chm(),
				'chxt'=>'x,y,r', // @todo configurable?
				'chxl'=>$this->series_x_labels(),
				'chxr'=>$this->series_scale(),
			);

		} else
			return array();
	}

	/**
	 * Encode the series data
	 */
	private function encode($data,$max=NULL) {
		$table = array();
		$table['simple'] = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
		$table['extend'] = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.';

		$size = array();
		$size['simple'] = strlen($table['simple']);
		$size['extend'] = strlen($table['extend']);

		if (is_null($max) OR $max == 0)
			$max = max($data) > 0 ? max($data) : 1;

		$encode = '';

		switch ($this->data_encode) {
			case 't' :
				return join(',',$data);

			case 's' :
				foreach ($data as $v)
					if ($v > -1)
						$encode .= substr($table['simple'],($size['simple']-1)*($v/$max),1);
					else
						$encode .= '_';

				break;

			case 'e' :
				foreach ($data as $v) {
					# Convert to a 0-4095 data range
					$y = 4095*$v/$max;
					$first = substr($table['extend'],floor($y/$size['extend']),1);
					$second = substr($table['extend'],$y%$size['extend'],1);
					$encode .= "$first$second";
				}

				break;
		}

		return $encode;
	}
}
?>
