<?php
/**
 * memory_table.php - Main class to deal with memory table update.
 * 						 This is a singleton therefore it is the same object throughout the request
 * 						 Necessary updates always take place at the end if the request except the first time after a MySQL DB restart.
 * 						 This can be utilized for any purpuse if table performance is a problem.
 * 						 Currently, only the unit table for searches has a performance problem.
 * 						 DO NOT USE THIS IF YOU DON'T KNOW WHAT YOU ARE DOING!
 *
 * @author $Author: dtong $
 * @version $Id: memory_table.php,v 1.4 2010/07/30 11:58:41 dtong Exp $
 * @copyright Copyright (c) 2010, Tiller Software Co., Ltd.
*/

class memory_table {
	static private $instance; //Do not assign default value for $instance
	private $table = '';
	private $update_sql = '';
	private $auto_update_flag = FALSE;
	private $min_auto_update_delta = 300; //No less than 5 minues for auto update
	private $auto_update_delta; //Default is in get_instance()
	private $update_flag = FALSE;
	private $min_update_delta = 3; //Has to be more than 3 sedonds before another update, very important in a high-load situtation
	private $update_delta; //Default is in get_instance()
	private $lock_name; //Global lock using MySQL get_lock(), this is a CMS application lock and have nothing to do with DB locking
	private $lock_prefix = 'memtab_'; //Avoid collision if other parts of the CMS start to use locks too.
	private $no_update_flag = FALSE; //Use it when it is updated outside of the destructor so no more updates
	private $mysql_lock_timeout = 3; //MySQL get_lock() will wait for no more than 3 seconds if couldn't get the lock
												//This should be a very low number becuase if it has to wait then that means someone else is updating
												//So, even if you get the lock then you shoud not do the update anyway. Only issue if a very slow server...
	/*
	private $auto_update_min_seed = 2; // Minumum seed value so chance of running the auto_update is 1 in 2
												  //Actual value is in auto_update()
	*/

	private $CI = NULL;

	//Private so it can be a singleton
	private function __construct($table, $update_sql, $check_empty, $auto_delta, $update_delta) {
		$this->table = $table;
		$this->update_sql = $update_sql;
		$this->lock_name = "{$this->lock_prefix}{$table}";

		if ( $auto_delta >= $this->min_auto_update_delta )
			$this->auto_update_delta = $auto_delta;
		else
			$this->auto_update_delta = $this->min_auto_update_delta;
		if ( $update_delta >= $this->min_update_delta )
			$this->update_delta = $update_delta;
		else
			$this->update_delta = $this->min_update_delta;

		$this->CI = & get_instance();
		$this->CI->load->model('cms/memory_table_model');

		if ( $check_empty && $this->is_empty() ) {
			//This is a forced update so be careful where this code is called first.
			//The best is to place it in the login success routine so you don't go into a race condition
			//The idea is that who ever logs in first then the table will get initilizes
			//The search handles have to check this too
			//This happens after every server reboot!
			$this->update_now(TRUE);
		}
	}

	public function __destruct() {
		//Always perform the actual update here after everything is finished
		//You can have more than 1 updates in a single request but the actual update only happens once here
		if ( $this->update_flag ) {
			//$this->update_now();
			//Sets the firty flag so the next auto update check would update the table
			//This is much more efficient in a multi-user environment
			//The data is not needed until we do a search so no need to always update it
			//Search handle base always does a auto_update with no delay.
			$this->CI->memory_table_model->set_dirty_flag($this->table);
			//return;
		}
		if ( $this->auto_update_flag )
			$this->auto_update_now();
	}

	//Can't clone this puppy
	public function __clone() {
        trigger_error('Clone is not allowed.', E_USER_ERROR);
    }

	function table() {
		$num = $this->CI->memory_table_model->current_table_number($this->table);
		if ( $num === FALSE )
			return FALSE;
		return "{$this->table}{$num}";
	}

	//Get the singelton object. The deltas are actually set here
	//$auto_delta=1800 - Automatically update the table in every 30 minutes based on whenever the auto_update is called
	//$update_delta=5 - Don't update if the last update is within the last 5 second.
	static function get_instance($table, $update_sql, $check_empty=FALSE, $auto_delta=1800, $update_delta=5) {
		if ( !isset(self::$instance) ) {
			$class = __CLASS__;
			self::$instance = new $class($table, $update_sql, $check_empty, $auto_delta, $update_delta);
		}
		return self::$instance;
	}

	//Are the memory tables empty
	function is_empty() {
		return $this->CI->memory_table_model->is_empty($this->table);
	}

	function update() {
		//This will cause the destructor to run the update
		$this->update_flag = TRUE;
	}

	//Use FALSE to turn off the seed. Default seed is 30 so 1 in 30 chance to run the auto_update.
	//function auto_update($seed=30) {
	function auto_update($delay=TRUE) {
		//The code that does the auto update checking is very efficient so just check every time.
		//Took out the random seed code
		/*
		//Don't want to run the auto_update check too often so use some random number to controll it
		if ( $seed !== FALSE && is_numeric($seed) ) {
			if ( $seed < $this->auto_update_min_seed )
				$seed = $this->auto_update_min_seed;
			$rand = rand(1,$seed);
			if ( $rand > 1 ) { //So, only execute if we get 1
				return;
			}
		}
		*/
		if ( !$delay )
			//Do the update now if auto update time is up
			$this->auto_update_now();
		else
		//This will cause the destructor to run the auto_update if the delta is reached
			$this->auto_update_flag = TRUE;
	}

	private function auto_update_now() {
		if ( $this->no_update_flag )
			return;
		$diff = $this->CI->memory_table_model->time_diff($this->table);
		$dirty = $this->CI->memory_table_model->is_dirty($this->table);
		if ( $diff >= $this->auto_update_delta || $dirty )
			$this->update_now();
	}

	//This function does the actual update, the update does not run if last update happen in time less than $this->update_delta
	//Forced update is used in the constructor when the memory tables are empty and no config entries.
	private function update_now($forced_update=FALSE) {
		if ( !$forced_update && $this->no_update_flag ) //In case update was executed in the current thread already
			return;
		$diff = $this->CI->memory_table_model->time_diff($this->table);
		if ( !$forced_update && $diff <= $this->update_delta )
			return FALSE;
		//Let's get an application lock and update the table
		if ( $this->CI->memory_table_model->lock($this->lock_name, $this->mysql_lock_timeout) ) {
			//This guarantees update for this memory table is safe in a multi-user environment
			//This is application locking and table or DB locking
			//Don't know what happens if the DB is distributed thru out multiple DB servers
			$this->CI->memory_table_model->update($this->table, $this->update_sql);
			$this->CI->memory_table_model->unlock($this->lock_name);
			//So we don't run the update twice in a single session
			$this->no_update_flag = TRUE;
			return TRUE;
		}
		osa_errorlog(__METHOD__ . " - Failed to obtained lock, table update for $this->table did not get executed.");
		return FALSE;
	}

}