<?php
/**
 * Cms_prefetch.php - DB Prefetch storage and retrieval library class. This assumes the developer
 * 						 fully understands the usage of this class and error checking is minimal.
 *
 * @author $Author: dtong $
 * @version $Id: Cms_prefetch.php,v 1.3 2011/05/10 16:11:56 dtong Exp $
 * @copyright Copyright (c) 2011, Tiller Software Co., Ltd.
*/
require_once('Cms_cache.php');

class Cms_prefetch extends Cms_cache {
	const EMPTY_DATA_SET = 2;
	const DB_PREFETCH_EXECUTED = 3;

	private $CI=NULL;
	private $prefetch_enabled=TRUE;
	private $method=FALSE;
	private $primaryid=FALSE;
	private $systemname=FALSE;
	private $subset_index=FALSE;
	private $sort_indexs=FALSE;
	//For external code to retrieve the proper types during a stage refresh
	private $stagenumber_hint = FALSE;

	function __construct() {
		parent::__construct();
		$this->CI = & get_instance();
		$this->prefetch_enabled = $this->CI->config->item('cms_db_prefetch_enable');
		$this->CI->load->model('cms/common_db_model');
	}

	//This is a major system class so protect it from lazy coders...
	function __set($var, $value) {
		echo __METHOD__ . " - You are not allowed to set undeclared class property '$var', bye bye.";
		exit;
	}

	/*
	 * $primaryid is usually courseid, unitid etc. This is the key with the same value for all your
	 * cache result set element.
	 *
	 * $systemname is usually the template name like ubd, btnsort-filehash etc.
	 *
	 * $type_index is the index name where the type is located or a index contains the key
	 * to identifier different sub-sets of data in the whole result set
	 */
	function reset($method, $primaryid, $systemname, $subset_index='type') {
		if ( !$this->prefetch_enabled ) {
			osa_log(__METHOD__, 'Prefetch is disabled.');
			return FALSE;
		}
		if ( !$method || !osa_is_int($primaryid) ) {
			osa_log(__METHOD__, 'Incorrect parameters.', $method, $primaryid);
			return FALSE;
		}
		$this->method=$method;
		$this->primaryid=$primaryid;
		$this->systemname=$systemname;
		$this->subset_index=$subset_index;
		$this->sort_indexs = FALSE;
		return TRUE;
	}

	//So we have a way to turn prefetch on or off for the whole system.
	//All callers have to check this function.
	function enabled() {
		return $this->prefetch_enabled;
	}

	//Enable the prefetch, depends on config.
	function set_enable() {
		$this->prefetch_enabled = $this->CI->config->item('cms_db_prefetch_enable');
	}

	//Disable prefetch for this session. Needed for stage refresh.
	function disable() {
		 $this->prefetch_enabled = FALSE;
	}

	//Sets the stage number for stage refresh. This is called from unitmain->render_stage()
	function set_stage_hint($stagenum) {
		if ( osa_is_int_one($stagenum) ) {
			$this->stagenumber_hint = $stagenum;
		}
	}

	//For prefetch routines
	function get_stage_hint() {
		return $this->stagenumber_hint;
	}

	//Runs the sql and then sort the result by index names in $sort_array
	//no need to put order by in your sql but make sure $sort_array is set properly.
	//The data has to be sorted by type to create subsets else it will break
	function set_sql($sql, $sort_array='type') {
		if ( !$this->prefetch_enabled ) {
			osa_log(__METHOD__, 'Prefetch is disabled.');
			return FALSE;
		}
		if ( !$this->method || !$this->primaryid ) {
			osa_log(__METHOD__, 'Reset was not called.');
			return FALSE;
		}
		if ( !$this->is_first_time() ) {
			return TRUE;
		}
		$ret = $this->CI->common_db_model->query($sql);
		if ( !$ret ) {
			$this->set_data(FALSE);
			return TRUE;
		}
		if ( !$this->set_data($ret, $sort_array) ) {
			//This will disable all subsequent prefetchs even from method not in the current method.
			$this->disable();
			return FALSE;
		}
		return TRUE;
	}

	//Sets the data set for later retrieval. Use this if you can't pass in the SQL
	//The data has to be sorted by type to create subsets else it will break
	public function set_data($data, $sort_array='type') {
		if ( !$this->prefetch_enabled ) {
			osa_log(__METHOD__, 'Prefetch is disabled.');
			return FALSE;
		}
		if ( !$this->method || !$this->primaryid ) {
			osa_log(__METHOD__, 'Reset was not called.');
			return FALSE;
		}
		if ( !$this->is_first_time() ) {
			return TRUE;
		}
		if ( !$data || !is_array($data) || count($data) <= 0 ) {
			//Tell this has no data
			$this->gen_add($this->method, $this->cache_stat_key(), self::EMPTY_DATA_SET);
			return TRUE;
		}
		//Check the first element to make sure we have the subset_index
		$current = reset($data);
		if ( !property_exists($current, $this->subset_index) ) {
			osa_log(__METHOD__, 'Missing subset_index.', $this->subset_index, $current);
			return FALSE;
		}

		if ( $sort_array ) {
			if ( !is_array($sort_array) ) {
				$sort_array = array($sort_array);
			}
			if ( !osa_array_natsort($data, $sort_array) ) {
				osa_log(__METHOD__, 'Failed to run osa_array_nat_sort.', $sort_index);
				$this->disable();
				return FALSE;
			}
		}

		//Tell this has been executed
		$this->gen_add($this->method, $this->cache_stat_key(), self::DB_PREFETCH_EXECUTED);
		$this->sort_indexs = $sort_array;
		return $this->save_data($data);
	}

	//Gets one subset
	function get($subset_value) {
		if ( !$this->prefetch_enabled ) {
			osa_log(__METHOD__, 'Prefetch is disabled.');
			return FALSE;
		}
		if ( $this->is_first_time() ) {
			osa_log(__METHOD__, 'get() has to be called after set().');
			return FALSE;
		}
		if ( $this->empty_data() ) {
			return FALSE;
		}
		//Not using reference/address on purpose, but the individual object values
		//can still change since they are always copy by reference.
		$one_set = FALSE;
		if ( !is_array($subset_value) ) {
			$subset_value = array($subset_value);
			$one_set = TRUE;
		}
		$ret = array();
		$sort_again = 0;
		foreach ($subset_value as $value) {
			$cache = $this->db_get($this->method, $this->cache_storage_key($value));
			if ( $one_set && $cache ) {
				$ret = & $cache;
			}
			elseif ( $cache ) {
				$ret = array_merge($ret, $cache);
				$sort_again++;
			}
		}
		if ( empty($ret) ) {
			return FALSE;
		}
		if ( $sort_again > 1 && $this->sort_indexs ) {
			//Merged 2 or more sets of of data results so sort again according to $this->sort_indexs
			//osa_array_natsort should not fail here since the set_data already ran once on the same data
			if ( !osa_array_natsort($ret, $this->sort_indexs) ) {
				osa_log(__METHOD__, 'Failed to run osa_array_natsort when it should not fail at all.', $this->sort_indexs, $ret);
				echo osa_ajaxmsg(lang('gen_internalservererror'));
				exit;
			}
		}
		return $ret;
	}

	//This is for the whole result set. If on a subset request and sees this then it should stop processing.
	private function empty_data() {
		$stat = $this->gen_get($this->method, $this->cache_stat_key());
		if ($stat == self::EMPTY_DATA_SET) {
			return TRUE;
		}
		return FALSE;
	}

	private function is_first_time() {
		$stat = $this->gen_get($this->method, $this->cache_stat_key());
		if ($stat === NULL) {
			return TRUE;
		}
		return FALSE;
	}

	private function cache_stat_key() {
		return "{$this->primaryid}*{$this->systemname}";
	}

	private function cache_storage_key($subset_value) {
		return "{$this->primaryid}*{$this->systemname}*{$subset_value}";
	}

	private function save_data(&$data) {
		$tmp = array();
		$subset_index = $this->subset_index;
		//Caller checked for empty array and $subset_index already so safe to do this
		$last_subset = reset($data)->$subset_index;
		foreach ($data as $key => $record) {
			/*if ($last_subset === FALSE) {
				$last_subset = $record->$subset_index;
			}*/
			if ( $last_subset != $record->$subset_index ) {
				$cachekey = $this->cache_storage_key($last_subset);
				$this->db_add($this->method, $cachekey, $tmp);
				$tmp = array();
				$last_subset = $record->$subset_index;
			}
			$tmp[] = $data[$key];
		}
		$cachekey = $this->cache_storage_key($record->$subset_index);
		$this->db_add($this->method, $cachekey, $tmp);
		return TRUE;
	}
}