<?php
/**
* @version	$Id: temp_handler.php 15055 2012-01-17 09:22:44Z alex $
* @package	In-Portal
* @copyright	Copyright (C) 1997 - 2009 Intechnic. All rights reserved.
* @license      GNU/GPL
* In-Portal is Open Source software.
* This means that this software may have been modified pursuant
* the GNU General Public License, and as distributed it includes
* or is derivative of works licensed under the GNU General Public License
* or other free or open source software licenses.
* See http://www.in-portal.org/license for copyright notices and details.
*/

defined('FULL_PATH') or die('restricted access!');

class kTempTablesHandler extends kBase {

	var $Tables = Array();

	/**
	 * Master table name for temp handler
	 *
	 * @var string
	 * @access private
	 */
	var $MasterTable = '';

	/**
	 * IDs from master table
	 *
	 * @var Array
	 * @access private
	 */
	var $MasterIDs = Array();

	var $AlreadyProcessed = Array();

	var $DroppedTables = Array();

	var $FinalRefs = Array();

	var $TableIdCounter = 0;

	var $CopiedTables =  Array();

	/**
	 * Foreign key cache
	 *
	 * @var Array
	 */
	var $FKeysCache = Array ();

	/**
	 * IDs of newly cloned items (key - prefix.special, value - array of ids)
	 *
	 * @var Array
	 */
	var $savedIDs = Array();

	/**
	 * Window ID of current window
	 *
	 * @var mixed
	 */
	var $WindowID = '';

	/**
	 * Event, that was used to create this object
	 *
	 * @var kEvent
	 * @access protected
	 */
	protected $parentEvent = null;

	/**
	 * Sets new parent event to the object
	 *
	 * @param kEvent $event
	 * @return void
	 * @access public
	 */
	public function setParentEvent(&$event)
	{
		$this->parentEvent =& $event;
	}

	function SetTables($tables)
	{
		// set table name as key for tables array
		$this->Tables = $tables;
		$this->MasterTable = $tables['TableName'];
	}

	function saveID($prefix, $special = '', $id = null)
	{
		if (!isset($this->savedIDs[$prefix.($special ? '.' : '').$special])) {
			$this->savedIDs[$prefix.($special ? '.' : '').$special] = array();
		}
		if (is_array($id)) {
			foreach ($id as $tmp_id => $live_id) {
				$this->savedIDs[$prefix.($special ? '.' : '').$special][$tmp_id] = $live_id;
			}
		}
		else {
			$this->savedIDs[$prefix.($special ? '.' : '').$special][] = $id;
		}
	}

	/**
	 * Get temp table name
	 *
	 * @param string $table
	 * @return string
	 */
	function GetTempName($table)
	{
		return $this->Application->GetTempName($table, $this->WindowID);
	}

	function GetTempTablePrefix()
	{
		return $this->Application->GetTempTablePrefix($this->WindowID);
	}

	/**
	 * Return live table name based on temp table name
	 *
	 * @param string $temp_table
	 * @return string
	 */
	function GetLiveName($temp_table)
	{
		return $this->Application->GetLiveName($temp_table);
	}

	function IsTempTable($table)
	{
		return $this->Application->IsTempTable($table);
	}

	/**
	 * Return temporary table name for master table
	 *
	 * @return string
	 * @access public
	 */
	function GetMasterTempName()
	{
		return $this->GetTempName($this->MasterTable);
	}

	function CreateTempTable($table)
	{
		$sql = 'CREATE TABLE ' . $this->GetTempName($table) . '
				SELECT *
				FROM ' . $table . '
				WHERE 0';

		$this->Conn->Query($sql);
	}

	function BuildTables($prefix, $ids)
	{
		$this->WindowID = $this->Application->GetVar('m_wid');

		$this->TableIdCounter = 0;
		$tables = Array(
				'TableName'	=>	$this->Application->getUnitOption($prefix, 'TableName'),
				'IdField'	=>	$this->Application->getUnitOption($prefix, 'IDField'),
				'IDs'		=>	$ids,
				'Prefix'	=>	$prefix,
				'TableId'	=>	$this->TableIdCounter++,
		);

		/*$parent_prefix = $this->Application->getUnitOption($prefix, 'ParentPrefix');
		if ($parent_prefix) {
			$tables['ForeignKey'] = $this->Application->getUnitOption($prefix, 'ForeignKey');
			$tables['ParentPrefix'] = $parent_prefix;
			$tables['ParentTableKey'] = $this->Application->getUnitOption($prefix, 'ParentTableKey');
		}*/

		$this->FinalRefs[ $tables['TableName'] ] = $tables['TableId']; // don't forget to add main table to FinalRefs too

		$sub_items = $this->Application->getUnitOption($prefix, 'SubItems', Array ());
		/* @var $sub_items Array */

		if ( is_array($sub_items) ) {
			foreach ($sub_items as $prefix) {
				$this->AddTables($prefix, $tables);
			}
		}

		$this->SetTables($tables);
	}

	/**
	 * Searches through TempHandler tables info for required prefix
	 *
	 * @param string $prefix
	 * @param Array $master
	 * @return mixed
	 */
	function SearchTable($prefix, $master = null)
	{
		if (is_null($master)) {
			$master = $this->Tables;
		}

		if ($master['Prefix'] == $prefix) {
			return $master;
		}

		if (isset($master['SubTables'])) {
			foreach ($master['SubTables'] as $sub_table) {
				$found = $this->SearchTable($prefix, $sub_table);
				if ($found !== false) {
					return $found;
				}
			}
		}

		return false;
	}

	function AddTables($prefix, &$tables)
	{
		if ( !$this->Application->prefixRegistred($prefix) ) {
			// allows to skip subitem processing if subitem module not enabled/installed
			return ;
		}

		$tmp = Array(
				'TableName' => $this->Application->getUnitOption($prefix,'TableName'),
				'IdField' => $this->Application->getUnitOption($prefix,'IDField'),
				'ForeignKey' => $this->Application->getUnitOption($prefix,'ForeignKey'),
				'ParentPrefix' => $this->Application->getUnitOption($prefix, 'ParentPrefix'),
				'ParentTableKey' => $this->Application->getUnitOption($prefix,'ParentTableKey'),
				'Prefix' => $prefix,
				'AutoClone' => $this->Application->getUnitOption($prefix,'AutoClone'),
				'AutoDelete' => $this->Application->getUnitOption($prefix,'AutoDelete'),
				'TableId' => $this->TableIdCounter++,
		);

		$this->FinalRefs[ $tmp['TableName'] ] = $tmp['TableId'];

		$constrain = $this->Application->getUnitOption($prefix, 'Constrain');
		if ( $constrain ) {
			$tmp['Constrain'] = $constrain;
			$this->FinalRefs[ $tmp['TableName'] . $tmp['Constrain'] ] = $tmp['TableId'];
		}

		$sub_items = $this->Application->getUnitOption($prefix, 'SubItems', Array ());
		/* @var $sub_items Array */

		if ( is_array($sub_items) ) {
			foreach ($sub_items as $prefix) {
				$this->AddTables($prefix, $tmp);
			}
		}

		if ( !is_array(getArrayValue($tables, 'SubTables')) ) {
			$tables['SubTables'] = Array ();
		}

		$tables['SubTables'][] = $tmp;
	}

	function CloneItems($prefix, $special, $ids, $master = null, $foreign_key = null, $parent_prefix = null, $skip_filenames = false)
	{
		if (!isset($master)) $master = $this->Tables;

		// recalling by different name, because we may get kDBList, if we recall just by prefix
		if (!preg_match('/(.*)-item$/', $special)) {
			$special .= '-item';
		}

		$object =& $this->Application->recallObject($prefix.'.'.$special, $prefix, Array('skip_autoload' => true, 'parent_event' => &$this->parentEvent));
		/* @var $object kCatDBItem */

		$object->PopulateMultiLangFields();

		foreach ($ids as $id) {
			$mode = 'create';
			$cloned_ids = getArrayValue($this->AlreadyProcessed, $master['TableName']);

			if ( $cloned_ids ) {
				// if we have already cloned the id, replace it with cloned id and set mode to update
				// update mode is needed to update second ForeignKey for items cloned by first ForeignKey
				if ( getArrayValue($cloned_ids, $id) ) {
					$id = $cloned_ids[$id];
					$mode = 'update';
				}
			}

			$object->Load($id);
			$original_values = $object->GetFieldValues();

			if (!$skip_filenames) {
				$object->NameCopy($master, $foreign_key);
			}
			elseif ($master['TableName'] == $this->MasterTable) {
				// kCatDBItem class only has this attribute
				$object->useFilenames = false;
			}

			if (isset($foreign_key)) {
				$master_foreign_key_field = is_array($master['ForeignKey']) ? $master['ForeignKey'][$parent_prefix] : $master['ForeignKey'];
				$object->SetDBField($master_foreign_key_field, $foreign_key);
			}

			if ($mode == 'create') {
				$this->RaiseEvent('OnBeforeClone', $master['Prefix'], $special, Array($object->GetId()), $foreign_key);
			}

			$object->inCloning = true;
			$res = $mode == 'update' ? $object->Update() : $object->Create();
			$object->inCloning = false;

			if ($res)
			{
				if ( $mode == 'create' && is_array( getArrayValue($master, 'ForeignKey')) ) {
					// remember original => clone mapping for dual ForeignKey updating
					$this->AlreadyProcessed[$master['TableName']][$id] = $object->GetId();
				}

				if ($mode == 'create') {
					$this->RaiseEvent('OnAfterClone', $master['Prefix'], $special, Array($object->GetId()), $foreign_key, array('original_id' => $id) );
					$this->saveID($master['Prefix'], $special, $object->GetID());
				}

				if ( is_array(getArrayValue($master, 'SubTables')) ) {
					foreach($master['SubTables'] as $sub_table) {
						if (!getArrayValue($sub_table, 'AutoClone')) continue;
						$sub_TableName = $object->IsTempTable() ? $this->GetTempName($sub_table['TableName']) : $sub_table['TableName'];

						$foreign_key_field = is_array($sub_table['ForeignKey']) ? $sub_table['ForeignKey'][$master['Prefix']] : $sub_table['ForeignKey'];
						$parent_key_field = is_array($sub_table['ParentTableKey']) ? $sub_table['ParentTableKey'][$master['Prefix']] : $sub_table['ParentTableKey'];

						if (!$foreign_key_field || !$parent_key_field) continue;

						$query = 'SELECT '.$sub_table['IdField'].' FROM '.$sub_TableName.'
											WHERE '.$foreign_key_field.' = '.$original_values[$parent_key_field];
						if (isset($sub_table['Constrain'])) $query .= ' AND '.$sub_table['Constrain'];

						$sub_ids = $this->Conn->GetCol($query);

						if ( is_array(getArrayValue($sub_table, 'ForeignKey')) ) {
							// $sub_ids could containt newly cloned items, we need to remove it here
							// to escape double cloning

							$cloned_ids = getArrayValue($this->AlreadyProcessed, $sub_table['TableName']);
							if ( !$cloned_ids ) $cloned_ids = Array();
							$new_ids = array_values($cloned_ids);
							$sub_ids = array_diff($sub_ids, $new_ids);
						}

						$parent_key = $object->GetDBField($parent_key_field);

						$this->CloneItems($sub_table['Prefix'], $special, $sub_ids, $sub_table, $parent_key, $master['Prefix']);
					}
				}
			}
		}

		if (!$ids) {
			$this->savedIDs[$prefix.($special ? '.' : '').$special] = Array();
		}

		return $this->savedIDs[$prefix.($special ? '.' : '').$special];
	}

	function DeleteItems($prefix, $special, $ids, $master=null, $foreign_key=null)
	{
		if ( !$ids ) {
			return;
		}

		if ( !isset($master) ) {
			$master = $this->Tables;
		}

		if ( strpos($prefix, '.') !== false ) {
			list($prefix, $special) = explode('.', $prefix, 2);
		}

		$prefix_special = rtrim($prefix . '.' . $special, '.');

		//recalling by different name, because we may get kDBList, if we recall just by prefix
		$recall_prefix = $prefix_special . ($special ? '' : '.') . '-item';
		$object =& $this->Application->recallObject($recall_prefix, $prefix, Array ('skip_autoload' => true, 'parent_event' => &$this->parentEvent));
		/* @var $object kDBItem */

		foreach ($ids as $id) {
			$object->Load($id);
			$original_values = $object->GetFieldValues();

			if ( !$object->Delete($id) ) {
				continue;
			}

			if ( is_array(getArrayValue($master, 'SubTables')) ) {
				foreach ($master['SubTables'] as $sub_table) {
					if ( !getArrayValue($sub_table, 'AutoDelete') ) {
						continue;
					}

					$sub_TableName = $object->IsTempTable() ? $this->GetTempName($sub_table['TableName']) : $sub_table['TableName'];

					$foreign_key_field = is_array($sub_table['ForeignKey']) ? getArrayValue($sub_table, 'ForeignKey', $master['Prefix']) : $sub_table['ForeignKey'];
					$parent_key_field = is_array($sub_table['ParentTableKey']) ? getArrayValue($sub_table, 'ParentTableKey', $master['Prefix']) : $sub_table['ParentTableKey'];

					if ( !$foreign_key_field || !$parent_key_field ) {
						continue;
					}

					$sql = 'SELECT ' . $sub_table['IdField'] . '
							FROM ' . $sub_TableName . '
							WHERE ' . $foreign_key_field . ' = ' . $original_values[$parent_key_field];
					$sub_ids = $this->Conn->GetCol($sql);

					$parent_key = $object->GetDBField(is_array($sub_table['ParentTableKey']) ? $sub_table['ParentTableKey'][$prefix] : $sub_table['ParentTableKey']);

					$this->DeleteItems($sub_table['Prefix'], $special, $sub_ids, $sub_table, $parent_key);
				}
			}

		}
	}

	function DoCopyLiveToTemp($master, $ids, $parent_prefix=null)
	{
		// when two tables refers the same table as sub-sub-table, and ForeignKey and ParentTableKey are arrays
		// the table will be first copied by first sub-table, then dropped and copied over by last ForeignKey in the array
		// this should not do any problems :)
		if ( !preg_match("/.*\.[0-9]+/", $master['Prefix']) ) {
			if( $this->DropTempTable($master['TableName']) )
			{
				$this->CreateTempTable($master['TableName']);
			}
		}

		if (is_array($ids)) {
			$ids = join(',', $ids);
		}

		$table_sig = $master['TableName'].(isset($master['Constrain']) ? $master['Constrain'] : '');

		if ($ids != '' && !in_array($table_sig, $this->CopiedTables)) {
			if ( getArrayValue($master, 'ForeignKey') ) {
				if ( is_array($master['ForeignKey']) ) {
					$key_field = $master['ForeignKey'][$parent_prefix];
				}
				else {
					$key_field = $master['ForeignKey'];
				}
			}
			else {
				$key_field = $master['IdField'];
			}

			$query = 'INSERT INTO '.$this->GetTempName($master['TableName']).'
									SELECT * FROM '.$master['TableName'].'
									WHERE '.$key_field.' IN ('.$ids.')';
			if (isset($master['Constrain'])) $query .= ' AND '.$master['Constrain'];
			$this->Conn->Query($query);

			$this->CopiedTables[] = $table_sig;

			$query = 'SELECT '.$master['IdField'].' FROM '.$master['TableName'].'
								WHERE '.$key_field.' IN ('.$ids.')';
			if (isset($master['Constrain'])) $query .= ' AND '.$master['Constrain'];
			$this->RaiseEvent( 'OnAfterCopyToTemp', $master['Prefix'], '', $this->Conn->GetCol($query) );
		}

		if ( getArrayValue($master, 'SubTables') ) {
			foreach ($master['SubTables'] as $sub_table) {

				$parent_key = is_array($sub_table['ParentTableKey']) ? $sub_table['ParentTableKey'][$master['Prefix']] : $sub_table['ParentTableKey'];
				if (!$parent_key) continue;

				if ( $ids != '' && $parent_key != $key_field ) {
					$query = 'SELECT '.$parent_key.' FROM '.$master['TableName'].'
										WHERE '.$key_field.' IN ('.$ids.')';
					$sub_foreign_keys = join(',', $this->Conn->GetCol($query));
				}
				else {
					$sub_foreign_keys = $ids;
				}
				$this->DoCopyLiveToTemp($sub_table, $sub_foreign_keys, $master['Prefix']);
			}
		}
	}

	function GetForeignKeys($master, $sub_table, $live_id, $temp_id=null)
	{
		$mode = 1; //multi
		if (!is_array($live_id)) {
			$live_id = Array($live_id);
			$mode = 2; //single
		}
		if (isset($temp_id) && !is_array($temp_id)) $temp_id = Array($temp_id);

		if ( isset($sub_table['ParentTableKey']) ) {
			if ( is_array($sub_table['ParentTableKey']) ) {
				$parent_key_field = $sub_table['ParentTableKey'][$master['Prefix']];
			}
			else {
				$parent_key_field = $sub_table['ParentTableKey'];
			}
		}
		else {
			$parent_key_field = $master['IdField'];
		}

		$cached = getArrayValue($this->FKeysCache, $master['TableName'].'.'.$parent_key_field);

		if ( $cached ) {
			if ( array_key_exists(serialize($live_id), $cached) ) {
				list($live_foreign_key, $temp_foreign_key) = $cached[serialize($live_id)];
				if ($mode == 1) {
					return $live_foreign_key;
				}
				else {
					return Array($live_foreign_key[0], $temp_foreign_key[0]);
				}
			}
		}

		if ($parent_key_field != $master['IdField']) {
			$query = 'SELECT '.$parent_key_field.' FROM '.$master['TableName'].'
								WHERE '.$master['IdField'].' IN ('.join(',', $live_id).')';
			$live_foreign_key = $this->Conn->GetCol($query);

			if (isset($temp_id)) {
				// because DoCopyTempToOriginal resets negative IDs to 0 in temp table (one by one) before copying to live
				$temp_key = $temp_id < 0 ? 0 : $temp_id;
				$query = 'SELECT '.$parent_key_field.' FROM '.$this->GetTempName($master['TableName']).'
									WHERE '.$master['IdField'].' IN ('.join(',', $temp_key).')';
				$temp_foreign_key = $this->Conn->GetCol($query);
			}
			else {
				$temp_foreign_key = Array();
			}
		}
		else {
			$live_foreign_key = $live_id;
			$temp_foreign_key = $temp_id;
		}

		$this->FKeysCache[$master['TableName'].'.'.$parent_key_field][serialize($live_id)] = Array($live_foreign_key, $temp_foreign_key);

		if ($mode == 1) {
			return $live_foreign_key;
		}
		else {
			return Array($live_foreign_key[0], $temp_foreign_key[0]);
		}
	}

	/**
	 * Copies data from temp to live table and returns IDs of copied records
	 *
	 * @param Array $master
	 * @param string $parent_prefix
	 * @param Array $current_ids
	 * @return Array
	 * @access public
	 */
	public function DoCopyTempToOriginal($master, $parent_prefix = null, $current_ids = Array())
	{
		if ( !$current_ids ) {
			$query = 'SELECT ' . $master['IdField'] . ' FROM ' . $this->GetTempName($master['TableName']);

			if ( isset($master['Constrain']) ) {
				$query .= ' WHERE ' . $master['Constrain'];
			}

			$current_ids = $this->Conn->GetCol($query);
		}

		$table_sig = $master['TableName'] . (isset($master['Constrain']) ? $master['Constrain'] : '');

		if ($current_ids) {
			// delete all ids from live table - for MasterTable ONLY!
			// because items from Sub Tables get deteleted in CopySubTablesToLive !BY ForeignKey!
			if ( $master['TableName'] == $this->MasterTable ) {
				$this->RaiseEvent('OnBeforeDeleteFromLive', $master['Prefix'], '', $current_ids);

				$query = 'DELETE FROM ' . $master['TableName'] . ' WHERE ' . $master['IdField'] . ' IN (' . join(',', $current_ids) . ')';
				$this->Conn->Query($query);
			}

			if ( getArrayValue($master, 'SubTables') ) {
				if ( in_array($table_sig, $this->CopiedTables) || $this->FinalRefs[$table_sig] != $master['TableId'] ) {
					return Array ();
				}

				foreach ($current_ids AS $id) {
					$this->RaiseEvent('OnBeforeCopyToLive', $master['Prefix'], '', Array ($id));

					//reset negative ids to 0, so autoincrement in live table works fine
					if ( $id < 0 ) {
						$query = '	UPDATE ' . $this->GetTempName($master['TableName']) . '
									SET ' . $master['IdField'] . ' = 0
									WHERE ' . $master['IdField'] . ' = ' . $id;

						if ( isset($master['Constrain']) ) {
							$query .= ' AND ' . $master['Constrain'];
						}

						$this->Conn->Query($query);
						$id_to_copy = 0;
					}
					else {
						$id_to_copy = $id;
					}

					//copy current id_to_copy (0 for new or real id) to live table
					$query = '	INSERT INTO ' . $master['TableName'] . '
								SELECT * FROM ' . $this->GetTempName($master['TableName']) . '
								WHERE ' . $master['IdField'] . ' = ' . $id_to_copy;
					$this->Conn->Query($query);

					$insert_id = $id_to_copy == 0 ? $this->Conn->getInsertID() : $id_to_copy;

					$this->saveID($master['Prefix'], '', array ($id => $insert_id));
					$this->RaiseEvent('OnAfterCopyToLive', $master['Prefix'], '', Array ($insert_id), null, Array ('temp_id' => $id));

					$this->UpdateForeignKeys($master, $insert_id, $id);

					//delete already copied record from master temp table
					$query = '	DELETE FROM ' . $this->GetTempName($master['TableName']) . '
								WHERE ' . $master['IdField'] . ' = ' . $id_to_copy;

					if ( isset($master['Constrain']) ) {
						$query .= ' AND ' . $master['Constrain'];
					}

					$this->Conn->Query($query);
				}

				$this->CopiedTables[] = $table_sig;

				// when all of ids in current master has been processed, copy all sub-tables data
				$this->CopySubTablesToLive($master, $current_ids);
			}
			elseif ( !in_array($table_sig, $this->CopiedTables) && ($this->FinalRefs[$table_sig] == $master['TableId']) ) { //If current master doesn't have sub-tables - we could use mass operations
				// We don't need to delete items from live here, as it get deleted in the beginning of the method for MasterTable
				// or in parent table processing for sub-tables
				$live_ids = Array ();
				$this->RaiseEvent('OnBeforeCopyToLive', $master['Prefix'], '', $current_ids);

				foreach ($current_ids as $an_id) {
					if ( $an_id > 0 ) {
						$live_ids[$an_id] = $an_id;
						// positive (already live) IDs will be copied in on query all togather below,
						// so we just store it here
						continue;
					}
					else { // zero or negative ids should be copied one by one to get their InsertId
						// resetting to 0 so it get inserted into live table with autoincrement
						$query = '	UPDATE ' . $this->GetTempName($master['TableName']) . '
									SET ' . $master['IdField'] . ' = 0
									WHERE ' . $master['IdField'] . ' = ' . $an_id;
						// constrain is not needed here because ID is already unique
						$this->Conn->Query($query);

						// copying
						$query = '	INSERT INTO ' . $master['TableName'] . '
									SELECT * FROM ' . $this->GetTempName($master['TableName']) . '
									WHERE ' . $master['IdField'] . ' = 0';
						$this->Conn->Query($query);

						$live_ids[$an_id] = $this->Conn->getInsertID(); //storing newly created live id

						//delete already copied record from master temp table
						$query = '	DELETE FROM ' . $this->GetTempName($master['TableName']) . '
									WHERE ' . $master['IdField'] . ' = 0';
						$this->Conn->Query($query);

						$this->UpdateChangeLogForeignKeys($master, $live_ids[$an_id], $an_id);
					}
				}

				// copy ALL records to live table
				$query = '	INSERT INTO ' . $master['TableName'] . '
							SELECT * FROM ' . $this->GetTempName($master['TableName']);

				if ( isset($master['Constrain']) ) {
					$query .= ' WHERE ' . $master['Constrain'];
				}

				$this->Conn->Query($query);

				$this->CopiedTables[] = $table_sig;
				$this->RaiseEvent('OnAfterCopyToLive', $master['Prefix'], '', $live_ids);

				$this->saveID($master['Prefix'], '', $live_ids);
				// no need to clear temp table - it will be dropped by next statement
			}
		}

		if ( $this->FinalRefs[ $master['TableName'] ] != $master['TableId'] ) {
			return Array ();
		}

		/*if ( is_array(getArrayValue($master, 'ForeignKey')) )	{ //if multiple ForeignKeys
			if ( $master['ForeignKey'][$parent_prefix] != end($master['ForeignKey']) ) {
				return; // Do not delete temp table if not all ForeignKeys have been processed (current is not the last)
			}
		}*/

		$this->DropTempTable($master['TableName']);
		$this->Application->resetCounters($master['TableName']);

		if ( !isset($this->savedIDs[ $master['Prefix'] ]) ) {
			$this->savedIDs[ $master['Prefix'] ] = Array ();
		}

		return $this->savedIDs[ $master['Prefix'] ];
	}

	/**
	 * Create separate connection for locking purposes
	 *
	 * @return kDBConnection
	 */
	function &_getSeparateConnection()
	{
		static $connection = null;

		if (!isset($connection)) {
			$connection =& $this->Application->makeClass( 'kDBConnection', Array (SQL_TYPE, Array (&$this->Application, 'handleSQLError')) );
			/* @var $connection kDBConnection */

			$connection->debugMode = $this->Application->isDebugMode();
			$connection->Connect(SQL_SERVER, SQL_USER, SQL_PASS, SQL_DB, true);
		}

		return $connection;
	}

	function UpdateChangeLogForeignKeys($master, $live_id, $temp_id)
	{
		if ($live_id == $temp_id) {
			return ;
		}

		$prefix = $master['Prefix'];
		$main_prefix = $this->Application->GetTopmostPrefix($prefix);
		$ses_var_name = $main_prefix . '_changes_' . $this->Application->GetTopmostWid($this->Prefix);
		$changes = $this->Application->RecallVar($ses_var_name);
		$changes = $changes ? unserialize($changes) : Array ();

		foreach ($changes as $key => $rec) {
			if ($rec['Prefix'] == $prefix && $rec['ItemId'] == $temp_id) {
				// main item change log record
				$changes[$key]['ItemId'] = $live_id;
			}

			if ($rec['MasterPrefix'] == $prefix && $rec['MasterId'] == $temp_id) {
				// sub item change log record
				$changes[$key]['MasterId'] = $live_id;
			}

			if (in_array($prefix, $rec['ParentPrefix']) && $rec['ParentId'][$prefix] == $temp_id) {
				// parent item change log record
				$changes[$key]['ParentId'][$prefix] = $live_id;

				if (array_key_exists('DependentFields', $rec)) {
					// these are fields from table of $rec['Prefix'] table!
					// when one of dependent fields goes into idfield of it's parent item, that was changed
					$parent_table_key = $this->Application->getUnitOption($rec['Prefix'], 'ParentTableKey');
					$parent_table_key = is_array($parent_table_key) ? $parent_table_key[$prefix] : $parent_table_key;

					if ($parent_table_key == $master['IdField']) {
						$foreign_key = $this->Application->getUnitOption($rec['Prefix'], 'ForeignKey');
						$foreign_key = is_array($foreign_key) ? $foreign_key[$prefix] : $foreign_key;

						$changes[$key]['DependentFields'][$foreign_key] = $live_id;
					}
				}
			}
		}

		$this->Application->StoreVar($ses_var_name, serialize($changes));
	}

	function UpdateForeignKeys($master, $live_id, $temp_id)
	{
		$this->UpdateChangeLogForeignKeys($master, $live_id, $temp_id);

		foreach ($master['SubTables'] as $sub_table) {
			$foreign_key_field =  is_array($sub_table['ForeignKey']) ? getArrayValue($sub_table, 'ForeignKey', $master['Prefix']) : $sub_table['ForeignKey'];

			if (!$foreign_key_field) {
				continue;
			}

			list ($live_foreign_key, $temp_foreign_key) = $this->GetForeignKeys($master, $sub_table, $live_id, $temp_id);

			//Update ForeignKey in sub TEMP table
			if ($live_foreign_key != $temp_foreign_key) {
				$query = 'UPDATE '.$this->GetTempName($sub_table['TableName']).'
									SET '.$foreign_key_field.' = '.$live_foreign_key.'
									WHERE '.$foreign_key_field.' = '.$temp_foreign_key;
				if (isset($sub_table['Constrain'])) $query .= ' AND '.$sub_table['Constrain'];
				$this->Conn->Query($query);
			}
		}
	}

	function CopySubTablesToLive($master, $current_ids) {
		foreach ($master['SubTables'] as $sub_table) {

			$table_sig = $sub_table['TableName'].(isset($sub_table['Constrain']) ? $sub_table['Constrain'] : '');

			// delete records from live table by foreign key, so that records deleted from temp table
			// get deleted from live
			if (count($current_ids) > 0  && !in_array($table_sig, $this->CopiedTables) ) {
				$foreign_key_field = is_array($sub_table['ForeignKey']) ? getArrayValue($sub_table, 'ForeignKey', $master['Prefix']) : $sub_table['ForeignKey'];
				if (!$foreign_key_field) continue;
				$foreign_keys = $this->GetForeignKeys($master, $sub_table, $current_ids);
				if (count($foreign_keys) > 0) {
					$query = 'SELECT '.$sub_table['IdField'].' FROM '.$sub_table['TableName'].'
										WHERE '.$foreign_key_field.' IN ('.join(',', $foreign_keys).')';
					if (isset($sub_table['Constrain'])) $query .= ' AND '.$sub_table['Constrain'];

					if ( $this->RaiseEvent( 'OnBeforeDeleteFromLive', $sub_table['Prefix'], '', $this->Conn->GetCol($query), $foreign_keys ) ){
						$query = 'DELETE FROM '.$sub_table['TableName'].'
											WHERE '.$foreign_key_field.' IN ('.join(',', $foreign_keys).')';
						if (isset($sub_table['Constrain'])) $query .= ' AND '.$sub_table['Constrain'];
						$this->Conn->Query($query);
					}
				}
			}
			//sub_table passed here becomes master in the method, and recursively updated and copy its sub tables
			$this->DoCopyTempToOriginal($sub_table, $master['Prefix']);
		}
	}

	/**
	 * Raises event using IDs, that are currently being processed in temp handler
	 *
	 * @param string $name
	 * @param string $prefix
	 * @param string $special
	 * @param Array $ids
	 * @param string $foreign_key
	 * @param Array $add_params
	 * @return bool
	 * @access protected
	 */
	protected function RaiseEvent($name, $prefix, $special, $ids, $foreign_key = null, $add_params = null)
	{
		if ( !is_array($ids) ) {
			return true;
		}

		$event_key = $prefix . ($special ? '.' : '') . $special . ':' . $name;
		$event = new kEvent($event_key);
		if ( isset($foreign_key) ) {
			$event->setEventParam('foreign_key', $foreign_key);
		}

		$set_temp_id = ($name == 'OnAfterCopyToLive') && (!is_array($add_params) || !array_key_exists('temp_id', $add_params));

		foreach ($ids as $index => $id) {
			$event->setEventParam('id', $id);

			if ( $set_temp_id ) {
				$event->setEventParam('temp_id', $index);
			}

			if ( is_array($add_params) ) {
				foreach ($add_params as $name => $val) {
					$event->setEventParam($name, $val);
				}
			}

			$this->Application->HandleEvent($event);
		}

		return $event->status == kEvent::erSUCCESS;
	}

	function DropTempTable($table)
	{
		if ( in_array($table, $this->DroppedTables) ) {
			return false;
		}

		$query = 'DROP TABLE IF EXISTS ' . $this->GetTempName($table);

		array_push($this->DroppedTables, $table);
		$this->DroppedTables = array_unique($this->DroppedTables);
		$this->Conn->Query($query);

		return true;
	}

	function PrepareEdit()
	{
		$this->DoCopyLiveToTemp($this->Tables, $this->Tables['IDs']);
		if ($this->Application->getUnitOption($this->Tables['Prefix'],'CheckSimulatniousEdit')) {
			$this->CheckSimultaniousEdit();
		}
	}

	function SaveEdit($master_ids = Array())
	{
		// SessionKey field is required for deleting records from expired sessions
		$conn =& $this->_getSeparateConnection();

		$sleep_count = 0;
		do {
			// acquire lock
			$conn->ChangeQuery('LOCK TABLES '.TABLE_PREFIX.'Semaphores WRITE');

			$sql = 'SELECT SessionKey
					FROM ' . TABLE_PREFIX . 'Semaphores
					WHERE (MainPrefix = ' . $conn->qstr($this->Tables['Prefix']) . ')';
			$another_coping_active = $conn->GetOne($sql);

			if ($another_coping_active) {
				// another user is coping data from temp table to live -> release lock and try again after 1 second
				$conn->ChangeQuery('UNLOCK TABLES');
				$sleep_count++;
				sleep(1);
			}
		} while ($another_coping_active && ($sleep_count <= 30));

		if ($sleep_count > 30) {
			// another coping process failed to finished in 30 seconds
			$error_message = $this->Application->Phrase('la_error_TemporaryTableCopyingFailed');
			$this->Application->SetVar('_temp_table_message', $error_message);

			return false;
		}

		// mark, that we are coping from temp to live right now, so other similar attempt (from another script) will fail
		$fields_hash = Array (
			'SessionKey' => $this->Application->GetSID(),
			'Timestamp' => adodb_mktime(),
			'MainPrefix' => $this->Tables['Prefix'],
		);

		$conn->doInsert($fields_hash, TABLE_PREFIX.'Semaphores');
		$semaphore_id = $conn->getInsertID();

		// unlock table now to prevent permanent lock in case, when coping will end with SQL error in the middle
		$conn->ChangeQuery('UNLOCK TABLES');

		$ids = $this->DoCopyTempToOriginal($this->Tables, null, $master_ids);

		// remove mark, that we are coping from temp to live
		$conn->Query('LOCK TABLES '.TABLE_PREFIX.'Semaphores WRITE');

		$sql = 'DELETE FROM ' . TABLE_PREFIX . 'Semaphores
				WHERE SemaphoreId = ' . $semaphore_id;
		$conn->ChangeQuery($sql);

		$conn->ChangeQuery('UNLOCK TABLES');

		return $ids;
	}

	function CancelEdit($master=null)
	{
		if (!isset($master)) $master = $this->Tables;
		$this->DropTempTable($master['TableName']);
		if ( getArrayValue($master, 'SubTables') ) {
			foreach ($master['SubTables'] as $sub_table) {
				$this->CancelEdit($sub_table);
			}
		}
	}

	/**
	 * Checks, that someone is editing selected records and returns true, when no one.
	 *
	 * @param Array $ids
	 *
	 * @return bool
	 */
	function CheckSimultaniousEdit($ids = null)
	{
		$tables = $this->Conn->GetCol('SHOW TABLES');
		$mask_edit_table = '/' . TABLE_PREFIX . 'ses_(.*)_edit_' . $this->MasterTable . '$/';

		$my_sid = $this->Application->GetSID();
		$my_wid = $this->Application->GetVar('m_wid');
		$ids = implode(',', isset($ids) ? $ids : $this->Tables['IDs']);
		$sids = Array ();
		if (!$ids) {
			return true;
		}

		foreach ($tables as $table) {
			if ( preg_match($mask_edit_table, $table, $rets) ) {
				$sid = preg_replace('/(.*)_(.*)/', '\\1', $rets[1]); // remove popup's wid from sid
				if ($sid == $my_sid) {
					if ($my_wid) {
						// using popups for editing
						if (preg_replace('/(.*)_(.*)/', '\\2', $rets[1]) == $my_wid) {
							// don't count window, that is being opened right now
							continue;
						}
					}
					else {
						// not using popups for editing -> don't count my session tables
						continue;
					}
				}

				$sql = 'SELECT COUNT(' . $this->Tables['IdField'] . ')
						FROM ' . $table . '
						WHERE ' . $this->Tables['IdField'] . ' IN (' . $ids . ')';
				$found = $this->Conn->GetOne($sql);

				if (!$found || in_array($sid, $sids)) {
					continue;
				}

				$sids[] = $sid;
			}
		}

		if ($sids) {
			// detect who is it
			$sql = 'SELECT
						CONCAT(IF (s.PortalUserId = ' . USER_ROOT . ', \'root\',
							IF (s.PortalUserId = ' . USER_GUEST . ', \'Guest\',
								CONCAT(u.FirstName, \' \', u.LastName, \' (\', u.Username, \')\')
							)
						), \' IP: \', s.IpAddress, \'\') FROM ' . TABLE_PREFIX . 'UserSessions AS s
					LEFT JOIN ' . TABLE_PREFIX . 'Users AS u
					ON u.PortalUserId = s.PortalUserId
					WHERE s.SessionKey IN (' . implode(',', $sids) . ')';
			$users = $this->Conn->GetCol($sql);

			if ($users) {
				$this->Application->SetVar('_simultanious_edit_message',
					sprintf($this->Application->Phrase('la_record_being_edited_by'), join(",\n", $users))
				);

				return false;
			}
		}

		return true;
	}

}