<?php

class API
{
	/**
	* @var mixed A link to the database object if one exists, otherwise false
	*/
	var $db = false;

	/**
	* @var boolean has the item been loaded from the database
	*/
	var $loaded = false;

	/**
	* @var string the full table name
	*/
	var $table = '';

	/**
	* @var string the prefix of the table
	*/
	var $tablePrefix = '';

	/**
	* @var boolean Is versioning on ? True = yes
	*/
	var $versioned = false;

	/**
	* @var integer how many versions to keep
	*/
	var $maxVersions = 5;

	/**
	* @var string the name of the field in the table which is the primary key
	*/
	var $pk = '';

	/**
	* @var string the last error which caused a return false
	*/
	var $error = '';
	
	/**
	* @var bool Whether or not to use the superglobal post vars or not. If false then will use the matching member variables for the object.
	*/	
	var $usePostVars = true;

	/**
	* @var array The storage of the post vars
	*/
	var $fieldArray = array();
	

	/**
	* Constructor - Initialize the things that can't be defined statically
	*
	* @return void
	*/
	function API()
	{
		$this->setupDatabase();
		
		//Sort out the field array for use throughout.
		if ($this->usePostVars) {
			$this->fieldArray = &$_POST;
		} else {
			foreach ($this->fields as $val) {
				$this->fieldArray[$val] = $this->$val; 
			}
		}
	}

	/**
	* Setup the default database settings in the class
	*
	* @return void
	*/
	function setupDatabase()
	{
		$this->db = $GLOBALS['AKB_DB'];
		$tableSuffix = str_replace('api_','',strtolower(get_class($this))).'s';
		$this->table = $GLOBALS['tablePrefix'].$tableSuffix;
		$this->tablePrefix = $GLOBALS['tablePrefix'];
	}

	/**
	* Dont save any of the database stuff when we get serialized
	*
	* @return array
	*/
	function __sleep()
	{
		return array('loaded');
	}

	/**
	* Reestablish database settings when being unserialized
	*
	* @return void
	*/
	function __wakeup()
	{
		$this->setupDatabase();
	}

	/**
	* Create a new item in the database
	*
	* @return mixed false if failed to create, the id of the item otherwise
	*/
	function create()
	{
		$first = true;
		$vals = array();

		// If the database connection isnt setup we can't proceed
		if (!$this->db) {
			$this->error = GetLang('apiNotConnectedToDB');
			return false;
		}
		
		if (!$this->usePostVars) {
			foreach ($this->fields as $val) {
				$this->fieldArray[$val] = $this->$val; 
			}			
		}

		$fields = $this->fields;
		
		$pk_key = array_search($this->pk, $fields);
		if ($pk_key !== false && $pk_key !== null) {
			unset($fields[$pk_key]);
		}

		// Build the query
		
		foreach ($fields as $field) {
			// If any of our fields arn't set its going to upset the column
			// count. Since the api doesn't have an inbuilt mechanism for
			// dealing with defaults we must fail here
			if (!isset($this->fieldArray[$field])) {
				$this->error = sprintf(GetLang('apiPostNotSet'), $field);
				return false;
			}
			
			if (method_exists($this, 'validate_'.$field)
				&& !call_user_func_array(array(&$this, 'validate_'.$field), array(&$this->fieldArray[$field]))) {
				if (empty($this->error)) {
					$this->error = sprintf(GetLang('apiValidateFailedFor'), $field);
				}
				
				return false;
			}
			$vals[$field] = $this->fieldArray[$field];
		}
		
		if ($this->db->InsertQuery($this->table, $vals)) {
			$newid = $this->db->LastId($this->table.'_seq');
			$vals[$this->pk] = $newid;
			$this->$pk_key = $newid;
			// If we are versioned run the history query
			if ($this->versioned) {
				$this->db->InsertQuery($this->table.'_history', $vals);
			}
			return $newid;
		} else {
			$this->error = GetLang('apiInsertFailed').' '.$GLOBALS['AKB_DB']->GetErrorMsg();;
			return false;
		}
	}
	
	/**
	* copy
	*
	* Copies an item in the database. On success will return the new object.
	*
	* @return the object (sub class) on success. Bool False on failure.
	*/
	function copy() {
		
		//Check that everything is loaded.
		if (!$this->loaded) {
			$this->error = GetLang('apiNotLoaded');
			return false;
		}

		//Get the primary key
		$pk = array($this->pk);
		
		$copy_query = 'INSERT INTO `'.$this->table.'`
		(`'.implode('`, `', $this->writeFields()).'`)
		SELECT `'.implode('`, `', $this->writeFields()).'`
		FROM `'.$this->table.'`
		WHERE '.$this->pk.' = '.((int) $this->{$this->pk});
	
		if ($this->db->Query($copy_query)) {
			$className = get_class($this);
			$tempObj = new $className;
			$tempObj->load($tempObj->db->LastId($this->table.'_seq'));

			return $tempObj;
		} else {
			return false;
		}
	}

	/**
	* Delete an item from the database
	*
	* @param integer $id If id is given and is positive delete the item $id
	*
	* @return boolean True if deletion successful
	*/
	function delete($id=0)
	{
		// Stop if the id isnt an pos integer
		if (!$this->is_positive_int($id)) {
			$this->error = GetLang('apiIdNotAPosInt');
			return false;
		}

		// If the database connection isnt setup we can't proceed
		if (!$this->db) {
			$this->error = GetLang('apiNotConnectedToDB');
			return false;
		}

		// Otherwise delete the item
		$query = 'DELETE
		FROM `'.$this->table.'`
		WHERE `'.$this->pk.'` = '.($id);
		
		if ($this->db->Query($query)) {
			return true;
		} else {
			$this->error = GetLang('apiDeleteFailed').' '.$GLOBALS['AKB_DB']->GetErrorMsg();;
			return false;
		}
	}

	/**
	* Delete multiple items in one database query, useful for bulk actions
	*
	* @param $ids array The array of ids to delete.
	*
	* @return boolean Return true on successful deletion
	*/
	function multiDelete($ids=0)
	{
		// If the database connection isnt setup we can't proceed
		if (!$this->db) {
			$this->error = GetLang('apiNotConnectedToDB');
			return false;
		}

		if (!is_array($ids)) {
			$this->error = GetLang('apiNotAnArray');
			return false;
		}

		if (empty($ids)) {
			return true;
		}

		// Make sure the array only contains integers
		foreach (array_keys($ids) as $key) {
			if (!is_numeric($key)) {
				$this->error = GetLang('apiIdNotAPosInt');
				return false;
			}
		}

		$query = 'DELETE
		FROM `'.$this->table.'`
		WHERE `'.$this->pk.'` IN ('.implode(',', array_keys($ids)).')';

		if ($this->db->Query($query)) {
			return true;
		} else {
			$this->error = GetLang('apiDeleteFailed').' '.$GLOBALS['AKB_DB']->GetErrorMsg();
			return false;
		}

	}

	/**
	* Save the loaded object to the database if any of the fields have been
	* modified since being loaded. If the item is versioned it will save a copy
	*
	* @return boolean True if the save succeeded
	*/
	function save()
	{
		// We can only save already loaded objects
		if (!$this->loaded) {
			$this->error = GetLang('apiNotLoaded');
			return false;
		}

		$pk = array ($this->pk);

		// remove the primary key from the field list
		$fields = array_diff($this->fields, $pk);

		foreach ($fields as $field) {
			// If the field has been modified update it
			if ((isset($this->fieldArray[$field]) && $this->$field != $this->fieldArray[$field]) || (!$this->usePostVars)) {
				if (!$this->updateField($field, $this->fieldArray[$field])) {
					return false;
				}
			}
		}

		if ($this->versioned) {
			$history_query = 'INSERT INTO `'.$this->table.'_history`
			(`'.implode('`, `', $this->fields).'`)
			SELECT `'.implode('`, `', $this->fields).'`
			FROM `'.$this->table.'`
			WHERE '.$this->pk.' = '.((int) $this->{$this->pk});

			$this->db->Query($history_query);

			$this->cleanupOldVersions();
		}

		return true;
	}

	/**
	* Update a database field.
	*
	* @param string $field the name of the field to update
	* @param string $value the value to update it with. This value will be put through db->Quote before saving
	*
	* @return boolean True if update was successful
	*/
	function updateField($field, $value)
	{
		// If the database connection isnt setup we can't proceed
		if (!$this->db) {
			$this->error = GetLang('apiNotConnectedToDB');
			return false;
		}

		// We can only do an update on loaded objects
		if (!$this->loaded) {
			$this->error = GetLang('apiNotLoaded');
			return false;
		}

		if (method_exists($this, 'validate_'.$field)
			&& !call_user_func(array($this, 'validate_'.$field), $value)) {
			$this->error = sprintf(GetLang('apiValidateFailedFor'), $field);
			return false;
		}

//		$query = 'UPDATE `'.$this->table.'`
//		SET `'.$field."` = '".$this->db->Quote($value)."'
//		WHERE `".$this->pk.'` = '.((int) $this->{$this->pk});
		
		if ($this->db->UpdateQuery($this->table, array($field=>$value), $this->pk.' = '.((int) $this->{$this->pk}))) {
			return true;
		} else {
			$this->error = GetLang('apiUpdateFailed').' '.$GLOBALS['AKB_DB']->GetErrorMsg();
			return false;
		}
	}

	/**
	* Update multiple database fields. If the item is versioned save a copy of
	* the update to the versioned table
	*
	* @param string $field the name of the field to update
	* @param string $value the value to update it with. This value will be put through db->Quote before saving
	* @param array $ids the ids to update
	*
	* @return boolean True if update was successful
	*/
	function multiUpdateField($field, $value, $ids, $use_quotes=true)
	{
		// If the database connection isnt setup we can't proceed
		if (!$this->db) {
			$this->error = GetLang('apiNotConnectedToDB');
			return false;
		}

		if (!is_array($ids)) {
			$this->error = GetLang('apiNotAnArray');
			return false;
		}

		if (empty($ids)) {
			return true;
		}

		// Make sure the array only contains integers
		foreach ($ids as $key => $val) {
			if (!is_numeric($key)) {
				$this->error = GetLang('apiIdNotAPosInt');
				return false;
			}
		}

		if (method_exists($this, 'validate_'.$field)
			&& !call_user_func(array($this, 'validate_'.$field), $value)) {
			$this->error = sprintf(GetLang('apiValidateFailedFor'), $field);
			return false;
		}

		$query = "UPDATE `".$this->table."` ";
		
		$success = $this->db->UpdateQuery($this->table, array($field=>$value), $this->pk." IN (".implode(',', array_keys($ids)).")");

		if ($this->versioned) {
			$history_query = 'INSERT INTO `'.$this->table.'_history`
			(`'.implode('`, `', $this->fields).'`)
			SELECT `'.implode('`, `', $this->fields).'`
			FROM `'.$this->table.'`
			WHERE `'.$this->pk.'` IN ('.implode(',', array_keys($ids)).')';

			$this->db->Query($history_query);

			$this->cleanupOldVersions();
		}
		return $success;
	}

	/**
	* Different approach to loading. Instead of using the pk to load, use another
	* field in the table which is also unique e.g. username
	*
	* @param string $field The name of the field to check
	* @param string $value The value to find in $field
	*
	* @return boolean True if the item was loaded from the database
	*/
	function find($field, $value)
	{
		// If the database connection isnt setup we can't proceed
		if (!$this->db) {
			$this->error = GetLang('apiNotConnectedToDB');
			return false;
		}

		$query = 'SELECT *
		FROM '.$this->table.'
		WHERE '.$field." = '".$this->db->Quote($value)."'";

		$result = $this->db->Query($query);

		// If we have fetched the wrong number of rows return false
		if ($this->db->CountResult($query) != 1) {
			$this->error = GetLang('apiWrongLoadCount');
			return false;
		}
		$row = $this->db->Fetch($result);

		// Setup this object
		foreach ($this->fields as $field) {
			$this->$field = $row[$field];
		}
		
		$this->loaded = true;
		return true;
	}

	/**
	* Load an item from the database by its pk
	*
	* @param integer $id The id of the item
	*
	* @return boolean True if it was loaded ok
	*/
	function load($id)
	{
		return $this->find($this->pk, $id);
	}
	
	/**
	* Load multiple items from the database
	*
	* @param array $ident_array The id of the item. If pkarray is false, this will not use a where clause and will return all records.
	* @param mixed $field The id of the item. If blank, then this will default to the PK. Otherwise you can specify which field you'd like to use.
	* @param boolean $pkAsKey If true the the key of the array will be the pk value. If false then will just be numeric array. 
	* @param string $orderFieldDirection optional field and direction as a string. eg 'customfieldid ASC'
	*
	* @return array An array of API objects
	*/	
	function loadMulti($ident_array=false,$field=false,$pkAsKey=false,$orderFieldDirection="") {
		
		$where_string = "";
		
		if (!$field) {
			$field = $this->pk;
		}
		
		if (is_array($ident_array)) {
			foreach (array_keys($ident_array) as $id) {
				$where_string .= "`".$field."`= '".$this->db->Quote($id)."' OR ";
			}
			
			if ($where_string == "") {
				return false;
			}
			
			$where_string = "WHERE ".$where_string;
			$where_string = substr($where_string,0,-4);
		}
		
		if ($orderFieldDirection != "") {
			$orderFieldDirection = "ORDER BY ".$orderFieldDirection;
		}
		
		$query = "
			SELECT *
			FROM `".$this->table."`
			".$where_string."
			".$orderFieldDirection."
		";
		
		$result = $this->db->Query($query);
		
		$objCollection = array();
		$pk = $this->pk;
		
		while ($tempRow = $this->db->fetch($result)) {
			$APIObj = $this;
			
			//Setup this object
			foreach ($APIObj->fields as $field) {
				$APIObj->$field = $tempRow[$field];
			}
			
			$APIObj->loaded = true;
			
			if ($pkAsKey) {
				$objCollection[$APIObj->$pk] = phpClone($APIObj);
			} else {
				$objCollection[] = phpClone($APIObj);
			}
		}
		
		return $objCollection;
	}

	/**
	* Revert this item to a previous version
	*
	* @param $versionid The id to revert to
	*
	* @return boolean True if the revert succeeded
	*/
	function revert($versionid)
	{
		// We can't revert if we arn't versioning
		if (!$this->versioned) {
			$this->error = GetLang('apiNotVersioned');
			return false;
		}

		// We are trying to restore to an invalid version
		if (!$this->is_positive_int($versionid)) {
			$this->error = GetLang('apiIdNotAPosInt');
			return false;
		}

		$query = 'SELECT *
		FROM `'.$this->table.'_history`
		WHERE `versionid`='.$versionid;

		$result = $this->db->Query($query);
		if ($this->db->CountResult($query) != 1) {
			$this->error = GetLang('apiWrongLoadCount');
			return false;
		}

		$row = $this->db->Fetch($result);

		foreach ($row as $k => $v) {
			$this->fieldArray[$k] = $v;
		}
		
		$this->versioned = false;
		
		return $this->save();
	}

	/**
	* Remove old versions of a versioned item from the database
	*
	* @return void
	*/
	function cleanupOldVersions()
	{
		$versionsToDel = array();
		$query = 'SELECT `versionid`, `workflowstatus`
		FROM `'.$this->table.'_history`
		WHERE `'.$this->pk.'` = '.((int) $this->{$this->pk}).'
		ORDER BY `versionid` DESC';
		$result = $this->db->Query($query);
		// If we have more versions then we want to keep
		if ($this->db->CountResult($query) > $this->maxVersions) {
			$count = 1;
			// Build a list of versions to delete
			$keep = true;
			while ($row = $this->db->Fetch($result)) {
				if ($count > $this->maxVersions) {
					if ($keep) {
						if ($row['workflowstatus'] == WF_STATUS_PENDING || $row['workflowstatus'] == WF_STATUS_REPENDING  || $row['workflowstatus'] == WF_STATUS_DISAPPROVED ) {
							$versionsToDel[] = $row['versionid'];
						} else {
							$keep = false;
						}
					} else {
						$versionsToDel[] = $row['versionid'];
					}
				}
				
				if ($count <= $this->maxVersions && ($row['workflowstatus'] == WF_STATUS_APPROVED || $row['workflowstatus'] == WF_STATUS_AUTO_APPROVED )) {
					$keep = false;
				}
				$count++;
			}

			// If we have versions to delete then delete them
			if (!empty($versionsToDel)) {
				$query = 'DELETE
				FROM `'.$this->table.'_history`
				WHERE `versionid` IN ('.implode(',', $versionsToDel).')';
				$this->db->Query($query);
			}
		}
	}
	
	/**
	* writeFields
	*
	* Returns the fields from the object ready for use in a query.
	* Minus the primary key.
	*
	* @param string $var
	*
	* @return bool
	*/
	function writeFields() {
		$fieldArray = array();
		foreach ($this->fields as $field) {
			if ($this->pk != $field) {
				$fieldArray[] = $field;				
			}
		}
		
		return $fieldArray;
	}

	/**
	* is_ip
	*
	* Check if a var is in a valid ipv4 format
	*
	* @param string $var
	*
	* @return bool
	*/
	function is_ip($var)
	{
		$octets = explode('.', $var);
		if (count($octets) != 4) {
			return false;
		}

		foreach ($octets as $octet) {
			if ((int) $octet != $octet) {
				return false;
			}

			if ($octet < 0) {
				return false;
			}

			if ($octet > 255) {
				return false;
			}
		}
		return true;
	}

	/**
	* is_int
	*
	* Check if a variable is an integer
	*
	* @param string $var
	*
	* @return bool
	*/
	function is_int($var)
	{
		return ((int) $var == $var);
	}

	/**
	* is_positive_int
	*
	* Check if a variable is a positive integer
	*
	* @param string $var
	* @param bool $include_zero
	*
	* @return bool
	*/
	function is_positive_int($var, $include_zero=true)
	{
		if (!$this->is_int($var)) {
			return false;
		}

		if ($var < 0) {
			return false;
		}

		if ($var > 0 || $include_zero) {
			return true;
		}

		return false;
	}

	/**
	* is_standard_date
	*
	* Ensure the var is in the standard date format (Y-m-d H:i:s)
	*
	* @param string $var
	*
	* @return bool
	*/
	function is_standard_date($var)
	{
		return preg_match('/\d{4}-\d{1,2}-\d{1,2} \d{1,2}:\d{1,2}:\d{1,2}/', $var);
	}

}


?>
