Your IP : 18.189.188.121


Current Path : /home/bitrix/ext_www/home-comfort.in.ua/bitrix/modules/sale/lib/location/
Upload File :
Current File : /home/bitrix/ext_www/home-comfort.in.ua/bitrix/modules/sale/lib/location/tree.php

<?php
/**
 * Nested sets table tree implementation
 * 
 * Tree struct and data fields are kept in a single table
 * 
 * This class is for internal use only, not a part of public API.
 * It can be changed at any time without notification.
 * 
 * @access private
 */
namespace Bitrix\Sale\Location;

use Bitrix\Main;
use Bitrix\Main\DB;
use Bitrix\Main\Entity;
use Bitrix\Main\Localization\Loc;

use Bitrix\Sale\Location\Util\Assert;
use Bitrix\Sale\Location\DB\BlockInserter;
use Bitrix\Sale\Location\DB\Helper;

Loc::loadMessages(__FILE__);

abstract class Tree extends Entity\DataManager
{
	const SORT_FREE_BEFORE = 		1;
	const SORT_FREE_AFTER = 		2;
	const SORT_HOLE_SIZE = 			10;
	const SORT_HOLE_SIZE_HALF = 	5;

	const BLOCK_INSERT_MTU =	 	9999;

	const SPACE_ADD = 				1;
	const SPACE_REMOVE = 			2;

	public static function add(array $data)
	{
		return self::addExtended($data);
	}

	/**
	 * Available keys in $additional
	 * REBALANCE - if set to true, method will rebalance tree after insertion
	*/
	public static function addExtended(array $data, array $additional = array())
	{
		$rebalance = !isset($additional['REBALANCE']) || $additional['REBALANCE'] !== false;

		// determine LEFT_MARGIN, RIGHT_MARGIN and DEPTH_LEVEL
		if($data['PARENT_ID'] = intval($data['PARENT_ID']))
		{
			// if we have PARENT_ID set, just use it`s info
			$node = self::getNodeInfo($data['PARENT_ID']);

			$needResort = true;

			$data['LEFT_MARGIN'] = $node['RIGHT_MARGIN'];
			$data['RIGHT_MARGIN'] = $node['RIGHT_MARGIN'] + 1;
			$data['DEPTH_LEVEL'] = $node['DEPTH_LEVEL'] + 1;
			$data['PARENT_ID'] = $node['ID'];
		}
		else
		{
			// otherwise, we assume we have "virtual root node", that has LEFT_MARGIN == 0 and RIGHT_MARGIN == +INFINITY
			// it allows us to have actually a forest, not a tree

			$rm = self::getMaxMargin();
			$needResort = false;

			$data['LEFT_MARGIN'] = $rm > 1 ? $rm + 1 : 1;
			$data['RIGHT_MARGIN'] = $rm > 1 ? $rm + 2 : 2;

			$data['DEPTH_LEVEL'] = 1;
			$data['PARENT_ID'] = 0;
		}

		// process insert options: INSERT_AFTER and INSERT_BEFORE
		//self::processInsertInstruction($data);

		$addResult = parent::add($data);

		if($addResult->isSuccess() && $needResort && $rebalance)
			self::rebalance($node, $addResult->getId());

		return $addResult;
	}

	protected static function rebalance($node, $id)
	{
		self::manageFreeSpace($node['RIGHT_MARGIN'], 2, self::SPACE_ADD, $id);
	}

	// we must guarantee tree integrity in any situation, so make low-level checking to prevent walking around
	public static function checkFields(Entity\Result $result, $primary, array $data)
	{
		parent::checkFields($result, $primary, $data);

		if(!($result instanceof Entity\UpdateResult)) // work out only when update()
			return;

		foreach (static::getEntity()->getFields() as $field)
		{
			if($field->getName() == 'PARENT_ID' && strlen($data['PARENT_ID']))
			{
				//it cant be parent for itself
				if(intval($primary['ID']) == intval($data['PARENT_ID']))
				{
					$result->addError(new Entity\FieldError(
						$field,
						Loc::getMessage('SALE_LOCATION_TREE_ENTITY_CANNOT_MOVE_STRAIGHT_TO_ITSELF_EXCEPTION'),
						Entity\FieldError::INVALID_VALUE
					));
				}
				else
				{
					try
					{
						$node = self::getNodeInfo($primary['ID']);
						$nodeDst = self::getNodeInfo($data['PARENT_ID']);

						// new parent cannot belong to node subtree
						if($node['PARENT_ID'] != $nodeDst['ID'])
						{
							if($nodeDst['LEFT_MARGIN'] >= $node['LEFT_MARGIN'] && $nodeDst['RIGHT_MARGIN'] <= $node['RIGHT_MARGIN'])
							{
								$result->addError(new Entity\FieldError(
									$field,
									Loc::getMessage('SALE_LOCATION_TREE_ENTITY_CANNOT_MOVE_TO_ITSELF_EXCEPTION'),
									Entity\FieldError::INVALID_VALUE
								));
							}
						}

					}
					catch(Main\SystemException $e)
					{
					}
				}
			}
		}
	}

	public static function update($primary, array $data)
	{
		return self::update($primary, $data);
	}

	/**
	 * Available keys in $additional
	 * REBALANCE - if set to true, method will rebalance tree after insertion
	*/
	public static function updateExtended($primary, array $data, array $additional = array())
	{
		$rebalance = !isset($additional['REBALANCE']) || $additional['REBALANCE'] !== false;
		$node = self::getNodeInfo($primary);

		if(isset($data['PARENT_ID']) && !strlen($data['PARENT_ID']))
			$data['PARENT_ID'] = 0;

		$updResult = parent::update($primary, $data);

		// if we have 'PARENT_ID' key in $data, and it was changed, we should relocate subtree
		if($updResult->isSuccess() && isset($data['PARENT_ID']) && (intval($node['PARENT_ID']) != intval($data['PARENT_ID'])) && $rebalance)
			self::moveSubtree($primary, $data['PARENT_ID']);

		return $updResult;
	}

	public static function delete($primary)
	{
		static::deleteExtended($primary);
	}

	/**
	 * Available keys in $additional
	 * REBALANCE - if set to true, method will rebalance tree after insertion
	 * DELETE_SUBTREE - if set to true, only node will be deleted, and it`s subtree left unattached
	 * @param $primary
	 * @param array $additional
	 * @return Entity\DeleteResult
	 * @throws Main\SystemException
	 * @throws Tree\NodeIncorrectException
	 * @throws Tree\NodeNotFoundException
	 * @throws \Exception
	 */
	public static function deleteExtended($primary, array $additional = array()) // here also could be an implementation of CHILDREN_REATTACH
	{
		$rebalance = !isset($additional['REBALANCE']) || $additional['REBALANCE'] !== false;
		$deleteSubtree = !isset($additional['DELETE_SUBTREE']) || $additional['DELETE_SUBTREE'] !== false;

		if($deleteSubtree)
		{
			// it means we want to delete not only the following node, but the whole subtree that belongs to it
			// note that with this option set to Y tree structure integrity will be compromised
			
			$node = self::getNodeInfo($primary);
			if(intval($node['ID']))
			{
				static::checkNodeThrowException($node);
				// low-level
				Main\HttpApplication::getConnection()->query('delete from '.static::getTableName().' where LEFT_MARGIN > '.$node['LEFT_MARGIN'].' and RIGHT_MARGIN < '.$node['RIGHT_MARGIN']);

				// and also remove free spece, if needed
				if($rebalance)
				{
					self::manageFreeSpace(
						$node['RIGHT_MARGIN'], 
						($node['RIGHT_MARGIN'] - $node['LEFT_MARGIN']) + 1,
						self::SPACE_REMOVE
					);
				}
			}
			else
			{
				throw new Tree\NodeNotFoundException(false, array('INFO' => array('ID' => $primary)));
			}
		}

		return parent::delete($primary);
	}

	/**
	 * This method is for internal use only. It may be changed without any notification further, or even mystically disappear.
	 * 
	 * @access private
	 */
	public static function getSubtreeRangeSqlForNode($primary, $node = array())
	{
		$primary = Assert::expectIntegerPositive($primary, '$primary');

		if(empty($node))
		{
			$node = self::getNodeInfo($primary);
			if(!intval($node['ID']))
			{
				throw new Tree\NodeNotFoundException(false, array('INFO' => array('ID' => $primary)));
			}
		}

		static::checkNodeThrowException($node);

		$query = new Main\Entity\Query(static::getEntity());
		$query->setSelect(array('ID'));
		$query->setFilter(array(
			'>LEFT_MARGIN' => $node['LEFT_MARGIN'],
			'<RIGHT_MARGIN' => $node['RIGHT_MARGIN']
		));

		return $query->getQuery();
	}

	public static function checkNodeIsParentOfNodeById($primary, $childPrimary, $behaviour = array('CHECK_DIRECT' => false))
	{
		$primary = Assert::expectIntegerPositive($primary, '$primary');
		$childPrimary = Assert::expectIntegerPositive($childPrimary, '$childPrimary');

		return static::checkNodeIsParentOfNodeByCondition(array('=ID' => $primary), array('=ID' => $childPrimary), $behaviour);
	}

	protected static function checkNodeIsParentOfNodeByCondition($parentNodeFilter, $nodeFilter, $behaviour = array('CHECK_DIRECT' => false))
	{
		$parent = static::getList(array('filter' => $parentNodeFilter, 'limit' => 1))->fetch();
		$child = static::getList(array('filter' => $nodeFilter, 'limit' => 1))->fetch();

		if(!intval($parent['ID']))
				throw new Main\SystemException('Node being checked not found');
		if(!intval($child['ID']))
				throw new Main\SystemException('Child node not found');

		if($behaviour['CHECK_DIRECT'])
			return $parent['ID'] == $child['PARENT_ID'];

		return $parent['LEFT_MARGIN'] < $child['LEFT_MARGIN'] && $parent['RIGHT_MARGIN'] > $child['RIGHT_MARGIN'];
	}

	// recalc left_margin & right_margin in the whole tree
	// strongly recommened to invoke only inside a transaction
	public static function resort($dontCareEvents = false)
	{
		$edges = array();
		$nodes = array();

		$res = parent::getList(array('select' => array('ID', 'PARENT_ID', 'LEFT_MARGIN', 'RIGHT_MARGIN')));
		while($item = $res->Fetch())
		{
			$nodes[$item['ID']] = array(
				'LEFT_MARGIN' => $item['LEFT_MARGIN'],
				'RIGHT_MARGIN' => $item['RIGHT_MARGIN']
			);

			if(!intval($item['PARENT_ID']))
				$edges['ROOT'][] = $item['ID'];
			else
				$edges[$item['PARENT_ID']][] = $item['ID'];
		}

		// walk tree in-deep to obtain correct margins
		self::walkTreeInDeep('ROOT', $edges, $nodes, 0, 0, $dontCareEvents);

		// now massively insert new values into the database, using a temporal table
		$tabName = 'b_sale_location_temp_'.rand(99, 9999);
		$entityTableName = static::getTableName();

		$dbConnection = Main\HttpApplication::getConnection();
 		
		$dbConnection->query("create table ".$tabName." (
			ID ".Helper::getSqlForDataType('int').",
			LEFT_MARGIN ".Helper::getSqlForDataType('int').",
			RIGHT_MARGIN ".Helper::getSqlForDataType('int').",
			DEPTH_LEVEL ".Helper::getSqlForDataType('int')."
		)");
		
		$handle = new BlockInserter(array(
			'tableName' => $tabName,
			'exactFields' => array(
				'ID' => array('data_type' => 'integer'),
				'LEFT_MARGIN' => array('data_type' => 'integer'),
				'RIGHT_MARGIN' => array('data_type' => 'integer'),
				'DEPTH_LEVEL' => array('data_type' => 'integer'),
			),
			'parameters' => array(
				'mtu' => self::BLOCK_INSERT_MTU
			)
		));
		foreach($nodes as $id => $node)
		{
			$node['ID'] = $id;
			$handle->insert($node);
		}
		$handle->flush();

		// merge temp table with location table
		Helper::mergeTables($entityTableName, $tabName, array(
			'LEFT_MARGIN' => 'LEFT_MARGIN',
			'RIGHT_MARGIN' => 'RIGHT_MARGIN',
			'DEPTH_LEVEL' => 'DEPTH_LEVEL'
		), array('ID' => 'ID'));

		$dbConnection->query("drop table {$tabName}");
	}

	public static function getPathToNode($primary, $parameters, $behaviour = array('SHOW_LEAF' => true))
	{
		$primary = Assert::expectIntegerPositive($primary, '$primary');
		if(!is_array($behaviour))
			$behaviour = array();
		if(!isset($behaviour['SHOW_LEAF']))
			$behaviour['SHOW_LEAF'] = true;

		return self::getPathToNodeByCondition(array('ID' => $primary), $parameters, $behaviour);
	}

	/**
	 * Fetches a parent chain of a specified node
	 * 
	 * Available keys in $behaviour
	 * SHOW_LEAF : if set to true, return node itself in the result
	 * 
	 * @access private
	 */
	public static function getPathToNodeByCondition($filter, $parameters = array(), $behaviour = array('SHOW_LEAF' => true))
	{
		$filter = Assert::expectNotEmptyArray($filter, '$filter');

		if(!is_array($behaviour))
			$behaviour = array();
		if(!isset($behaviour['SHOW_LEAF']))
			$behaviour['SHOW_LEAF'] = true;

		if(empty($parameters))
			$parameters = array();

		// todo: try to do this job in a single query with join. Speed profit?

		$node = self::getList(array('filter' => $filter, 'limit' => 1))->fetch();
		if(!isset($node['ID']))
			throw new Main\SystemException(Loc::getMessage('SALE_LOCATION_TREE_ENTITY_NODE_NOT_FOUND_EXCEPTION'));

		$parameters['filter']['<=LEFT_MARGIN'] = intval($node['LEFT_MARGIN']);
		$parameters['filter']['>=RIGHT_MARGIN'] = intval($node['RIGHT_MARGIN']);

		if(!$behaviour['SHOW_LEAF'])
			$parameters['filter']['!=ID'] = $node['ID'];

		$parameters['order'] = array(
			'LEFT_MARGIN' => 'asc'
		);

		return self::getList($parameters);
	}

	public static function getPathToMultipleNodes($nodeInfo = array(), $parameters = array(), $behaviour = array('SHOW_LEAF' => true))
	{
		Assert::expectNotEmptyArray($nodeInfo, '$nodeInfo');

		if(!is_array($behaviour))
			$behaviour = array();
		if(!isset($behaviour['SHOW_LEAF']))
			$behaviour['SHOW_LEAF'] = true;

		if(empty($parameters))
			$parameters = array();

		if(is_array($parameters['select']))
			$originSelect = $parameters['select'];
		else
			$originSelect = array();

		if(is_array($parameters['order']))
			throw new Main\NotSupportedException('"Order" clause is not supported here');

		$parameters['order'] = array(
			'LEFT_MARGIN' => 'asc'
		);
		$parameters['select'][] = 'ID';
		$parameters['select'][] = 'PARENT_ID';

		$filter = array();
		foreach($nodeInfo as $node)
		{
			Assert::expectNotEmptyArray($node, '$nodeInfo[]');
			$node['ID'] = Assert::expectIntegerPositive($node['ID'], '$nodeInfo[][ID]');
			$node['LEFT_MARGIN'] = Assert::expectIntegerNonNegative($node['LEFT_MARGIN'], '$nodeInfo[][LEFT_MARGIN]');
			$node['RIGHT_MARGIN'] = Assert::expectIntegerPositive($node['RIGHT_MARGIN'], '$nodeInfo[][RIGHT_MARGIN]');

			$filter[] = array(
				'<=LEFT_MARGIN' => intval($node['LEFT_MARGIN']),
				'>=RIGHT_MARGIN' => intval($node['RIGHT_MARGIN'])
			);

			if(!$behaviour['SHOW_LEAF'])
				$filter['!=ID'] = $node['ID'];
		}
		$filter['LOGIC'] = 'OR';

		$parameters['filter'][] = $filter;

		$res = self::getList($parameters);

		$index = array();

		while($item = $res->Fetch())
		{
			$index[$item['ID']] = array(
				'NODE' => $item,
				'PARENT_ID' => $item['PARENT_ID']
			);
		}

		$depthLimit = count($index);

		$pathes = array();
		foreach($nodeInfo as $node)
		{
			$path = array();
			$id = $node['ID'];

			$i = 0;
			while($id)
			{
				if($i >= $depthLimit) // there is a cycle or smth like, anyway this is abnormal situation
					break;

				if(!isset($index[$id])) // non-existing element in the chain. strange, abort
					break;

				$resultNode = $index[$id]['NODE'];
				if(!in_array('PARENT_ID', $originSelect))
					unset($resultNode['PARENT_ID']);
				if(!in_array('ID', $originSelect))
					unset($resultNode['ID']);
				$path[$id] = $resultNode;

				$id = intval($index[$id]['PARENT_ID']);

				$i++;
			}

			$pathes[$node['ID']] = array(
				'ID' => $node['ID'],
				'PATH' =>  $path
			);
		}

		return new DB\ArrayResult($pathes);
	}

	public static function getDeepestCommonParent($nodeInfo = array(), $parameters = array())
	{
		Assert::expectNotEmptyArray($nodeInfo, '$nodeInfo');

		$filter = array();

		$min = false;
		$max = false; 
		foreach($nodeInfo as $node)
		{
			Assert::expectNotEmptyArray($node, '$nodeInfo[]');
			$node['LEFT_MARGIN'] = Assert::expectIntegerNonNegative($node['LEFT_MARGIN'], '$nodeInfo[][LEFT_MARGIN]');
			$node['RIGHT_MARGIN'] = Assert::expectIntegerPositive($node['RIGHT_MARGIN'], '$nodeInfo[][RIGHT_MARGIN]');

			if($min === false || $node['LEFT_MARGIN'] < $min)
				$min = $node['LEFT_MARGIN'];

			if($max === false || $node['RIGHT_MARGIN'] > $max)
				$max = $node['RIGHT_MARGIN'];
		}

		if(empty($parameters))
			$parameters = array();

		if(!is_array($parameters['order']))
			$parameters['order'] = array();

		$parameters['filter']['<LEFT_MARGIN'] = $min;
		$parameters['filter']['>RIGHT_MARGIN'] = $max;

		$parameters['order'] = array_merge(array(
			'LEFT_MARGIN' => 'desc',
			'RIGHT_MARGIN' => 'asc'
		), $parameters['order']);

		$parameters['limit'] = 1;

		return static::getList($parameters);
	}

	public static function getChildren($primary, $parameters = array())
	{
		if(empty($parameters))
			$parameters = array();

		if($primary = intval($primary)) // here $primary might be unset: in this case we take the first level of a tree
		{
			$node = self::getNodeInfo($primary);

			$parameters['filter']['>=LEFT_MARGIN'] = intval($node['LEFT_MARGIN']);
			$parameters['filter']['<=RIGHT_MARGIN'] = intval($node['RIGHT_MARGIN']);
			$parameters['filter']['!=ID'] = $primary;
			$parameters['filter']['DEPTH_LEVEL'] = intval($node['DEPTH_LEVEL']) + 1;
		}
		else
			$parameters['filter']['DEPTH_LEVEL'] = 1;

		return self::getList($parameters);
	}

	/**
	* Fetches a set of items which form sub-tree of a given node
	*/
	public static function getSubTree($primary, $parameters = array())
	{
		if(empty($parameters))
			$parameters = array();

		if($primary = intval($primary)) // here $primary might be unset: if so, get the whole tree
		{
			$node = self::getNodeInfo($primary);

			$parameters['filter']['>=LEFT_MARGIN'] = intval($node['LEFT_MARGIN']);
			$parameters['filter']['<=RIGHT_MARGIN'] = intval($node['RIGHT_MARGIN']);
		}

		if(!is_array($parameters['order']) || empty($parameters['order']))
			$parameters['order'] = array('LEFT_MARGIN' => 'asc');

		return self::getList($parameters);
	}

	/**
	* Fetches a chain of parents with their subtrees expanded
	* 
	* Available keys in $behaviour
	* SHOW_CHILDREN : if set to true, do return direct ancestors of $primary in the result
	* START_FROM
	*/
	public static function getParentTree($primary, $parameters = array(), $behaviour = array('SHOW_CHILDREN' => true, 'START_FROM' => false))
	{
		$primary = Assert::expectIntegerPositive($primary, '$primary');

		if(!is_array($behaviour))
			$behaviour = array();
		if(!isset($behaviour['SHOW_CHILDREN']))
			$behaviour['SHOW_CHILDREN'] = true;
		if(!isset($behaviour['START_FROM']))
			$behaviour['START_FROM'] = false;

		if(empty($parameters))
			$parameters = array();

		$startFrom = intval($behaviour['START_FROM']);
		$showChildren = $behaviour['SHOW_CHILDREN'];

		if(!$startFrom)
		{
			$conditions[] = array(
				'DEPTH_LEVEL' => 1
			);
		}

		// todo: combine (1) and (2) in one query, check perfomance change

		// (1)
		$res = self::getPathToNode($primary, array(
			'select' => array('ID')
		));

		$started = !$startFrom;
		while($item = $res->Fetch())
		{
			if($item['ID'] == $startFrom)
				$started = true;

			if(!$started)
				continue;

			if(!$showChildren && $item['ID'] == $primary)
				continue;

			$conditions[] = array(
				'PARENT_ID' => $item['ID']
			);
		}

		$conditions['LOGIC'] = 'OR';

		$parameters['filter'][] = $conditions;

		if(!is_array($parameters['order']) || empty($parameters['order']))
			$parameters['order'] = array('LEFT_MARGIN' => 'asc');

		// (2)
		return self::getList($parameters);
	}

	/////////////////////////
	/// PROTECTED
	/////////////////////////

	/**
	 * Do not call directly, only inside update()
	 * 
	 * @param int $primary Subtree`s root id to move
	 * @param int $primaryDst Item id to attach our subtree to
	 *
	 */
	protected final static function moveSubtree($primary, $primaryDst)
	{
		$node = self::getNodeInfo($primary);

		if(!($primaryDst = intval($primaryDst))) // move to root
		{
			$rm = self::getMaxMargin();

			$lDst = $rm + 1;
			$rDst = $rm + 2;
			$dDst = 0;
		}
		else
		{
			$nodeDst = self::getNodeInfo($primaryDst);

			$lDst = intval($nodeDst['LEFT_MARGIN']);
			$rDst = intval($nodeDst['RIGHT_MARGIN']);
			$dDst = intval($nodeDst['DEPTH_LEVEL']);
		}

		$lSub = intval($node['LEFT_MARGIN']);
		$rSub = intval($node['RIGHT_MARGIN']);
		$dSub = intval($node['DEPTH_LEVEL']);

		$tableName = static::getTableName();

		$sql = 	"update ".$tableName." set 

				DEPTH_LEVEL	= 
					case 
						when 
							LEFT_MARGIN between {$lSub} and {$rSub} 
						then 
							DEPTH_LEVEL + ".($dDst - $dSub + 1)."

						else 
							DEPTH_LEVEL 

					end, ";

		// DO NOT switch the column update order in the code below, it WILL NOT work correctly

		// subtree moves upwards along it`s path
		if ($lDst < $lSub && $rDst > $rSub && $dDst < ($dSub - 1))
		{
			$sql .= "

					RIGHT_MARGIN = 
						case 
							when
								RIGHT_MARGIN between ".($rSub + 1)." and ".($rDst - 1)."
							then
								RIGHT_MARGIN - ".($rSub - $lSub + 1)."

							when 
								LEFT_MARGIN between ".$lSub." and ".$rSub."
							then 
								RIGHT_MARGIN + ".((($rDst - $rSub - $dSub + $dDst) / 2) * 2 + $dSub - $dDst - 1)."

							else RIGHT_MARGIN
						end,

					LEFT_MARGIN =
						case 
							when 
								LEFT_MARGIN between ".($rSub + 1)." and ".($rDst - 1)." 
							then 
								LEFT_MARGIN - ".($rSub - $lSub + 1)." 

							when 
								LEFT_MARGIN between ".$lSub." and ".$rSub."
							then 
								LEFT_MARGIN + ".((($rDst - $rSub - $dSub + $dDst) / 2) * 2 + $dSub - $dDst - 1)."

							else 
								LEFT_MARGIN 
						end

					where LEFT_MARGIN between ".($lDst + 1)." and ".($rDst - 1);
		}
		elseif($lDst < $lSub) // subtree moves to the left of it`s path (to the left branch)
		{
			$sql .= "

					LEFT_MARGIN	= 
						case 
							when 
								LEFT_MARGIN between ".$rDst." and ".($lSub-1)."
							then 
								LEFT_MARGIN + ".($rSub - $lSub + 1)."

							when 
								LEFT_MARGIN between ".$lSub." and ".$rSub." 
							then 
								LEFT_MARGIN - ".($lSub - $rDst)."

							else 
								LEFT_MARGIN 
						end, 

					RIGHT_MARGIN = 
						case 
							when 
								RIGHT_MARGIN between ".$rDst." and ".$lSub." 
							then
								RIGHT_MARGIN + ".($rSub - $lSub + 1)."

							when
								RIGHT_MARGIN between ".$lSub." and ".$rSub."
							then
								RIGHT_MARGIN - ".($lSub - $rDst)." 

							else 
								RIGHT_MARGIN
						end 

					where LEFT_MARGIN between ".$lDst." and ".$rSub." or RIGHT_MARGIN between ".$lDst." and ".$rSub;
		}
		else // subtree moves to the right of it`s path (to the right branch)
		{
			$sql .= "

					LEFT_MARGIN	=
						case 
							when 
								LEFT_MARGIN between ".$rSub." and ".$rDst." 
							then 
								LEFT_MARGIN - ".($rSub - $lSub + 1)."

							when 
								LEFT_MARGIN between ".$lSub." and ".$rSub."
							then 
								LEFT_MARGIN + ".($rDst - $rSub - 1)." 
							
							else 
								LEFT_MARGIN 
						end, 

					RIGHT_MARGIN = 
						case 
							when 
								RIGHT_MARGIN between ".($rSub + 1)." and ".($rDst - 1)."
							then RIGHT_MARGIN - ".($rSub - $lSub + 1)."

							when 
								RIGHT_MARGIN between ".$lSub." and ".$rSub."
							then RIGHT_MARGIN + ".($rDst - $rSub - 1)." 

							else RIGHT_MARGIN
					end 

				where LEFT_MARGIN between ".$lSub." and ".$rDst." or RIGHT_MARGIN between ".$lSub." and ".$rDst;
		}

		Main\HttpApplication::getConnection()->query($sql);
	}

	protected final static function processInsertInstruction(&$data)
	{
		$data['INSERT_AFTER'] = intval($data['INSERT_AFTER']);
		$data['INSERT_BEFORE'] = intval($data['INSERT_BEFORE']);

		if($data['INSERT_AFTER'] || $data['INSERT_BEFORE'])
		{
			$neighbourId = $data['INSERT_BEFORE'] ? $data['INSERT_BEFORE'] : $data['INSERT_AFTER'];

			$sort = self::makeSortSpace(
				$neighbourId, 
				($data['INSERT_BEFORE'] ? self::SORT_FREE_BEFORE : self::SORT_FREE_AFTER),
				$data['PARENT_ID'], 
				isset($data['SORT']) ? $data['SORT'] : false
			);

			unset($data['INSERT_AFTER']);
			unset($data['INSERT_BEFORE']);

			if($sort != false)
				$data['SORT'] = $sort;
		}
	}

	protected final static function manageFreeSpace($right, $length = 2, $op = self::SPACE_ADD, $exceptId = false)
	{
		if($length <= 1 || $right <= 0)
			return;

		// LEFT_MARGIN & RIGHT_MARGIN are system fields, user should not know about them ever, so no orm events needed to be fired on update of them

		$sign = $op == self::SPACE_ADD ? '+' : '-';

		$tableName = static::getTableName();
		$exceptId = intval($exceptId);

		$query = "update {$tableName} set 
			LEFT_MARGIN = case when LEFT_MARGIN > {$right} then LEFT_MARGIN {$sign} {$length} else LEFT_MARGIN end,
			RIGHT_MARGIN = case when RIGHT_MARGIN >= {$right} then RIGHT_MARGIN {$sign} {$length} else RIGHT_MARGIN end 
			where RIGHT_MARGIN >= {$right}".($exceptId ? " and ID <> {$exceptId}" : "");
			
		$shifted = Main\HttpApplication::getConnection()->query($query);

		if(!$shifted)
			throw new Main\SystemException('Query failed: managing free space in a tree', 0, __FILE__, __LINE__); // SaleTreeSystemException
	}

	// act in assumption sort field is always defined for each node and also it`s value positive signed
	protected final static function makeSortSpace($primary, $direction = self::SORT_FREE_AFTER, $primaryParent, $knownSort = false)
	{
		$primary = Assert::expectIntegerPositive($primary, '$primary');
		$primaryParent = Assert::expectIntegerPositive($primary, '$primaryParent');

		$nodeFound = false;
		$sorts = array();

		$nextNodeId = false;
		$prevNodeId = false;

		$prev = false;
		$res = self::getChildren($primaryParent, array('select' => array('ID', 'SORT', 'CODE'), 'order' => array('SORT' => 'asc')));
		while($item = $res->Fetch())
		{
			if($nodeFound && !$nextNodeId)
				$nextNodeId = $item['ID'];

			if($item['ID'] == $primary)
			{
				$nodeFound = true;
				$prevNodeId = $prev;
			}

			$sorts[$item['ID']] = $item['SORT'];

			$prev = $item['ID'];
		}

		// no node exists or they are not neighbours
		if(!$nodeFound) 
			return false;

		// add extra items
		if(!$prevNodeId)
		{
			$sorts = array('FH' => 0) + $sorts;
			$prevNodeId = 'FH';
		}
		if(!$nextNodeId)
		{
			$sorts['FT'] = PHP_INT_MAX;
			$nextNodeId = 'FT';
		}

		// handle some obvious situations
		if($direction == self::SORT_FREE_BEFORE)
		{
			if($knownSort && ($knownSort < $sorts[$prevNodeId]) && ($knownSort < $sorts[$primary]))
				return $knownSort; // its okay, current sort fits

			// inequation above is not true, but there is free space between nodes
			if($sorts[$primary] - $sorts[$prevNodeId] > 1)
				return $sorts[$prevNodeId] + 1;

			$startShift = $primary;
			$return = $sorts[$prevNodeId] + self::SORT_HOLE_SIZE_HALF;
		}
		else
		{
			if($knownSort && ($knownSort < $sorts[$primary]) && ($knownSort < $sorts[$nextNodeId]))
				return $knownSort; // its okay, current sort fits

			// inequation above is not true, but there is free space between nodes
			if($sorts[$nextNodeId] - $sorts[$primary] > 1)
				return $sorts[$primary] + 1;

			$startShift = $nextNodeId;
			$return = $sorts[$primary] + self::SORT_HOLE_SIZE_HALF;
		}

		// .. or else we forced to make a hole
		$begin = false;
		$shift = $sorts[$startShift] + self::SORT_HOLE_SIZE;

		foreach($sorts as $id => $sVal)
		{
			if($id == $startShift)
				$begin = true;

			if($begin && $sVal <= $shift)
			{
				$shift = $sVal + self::SORT_HOLE_SIZE;
				parent::update($id, array('SORT' => $shift));
			}
		}

		return $return;
	}

	// in-deep tree walk
	protected final static function walkTreeInDeep($primary, $edges, &$nodes, $margin, $depth = 0, $dontCareEvents = false)
	{
		$lMargin = $margin;

		if(empty($edges[$primary]))
			$rMargin = $margin + 1;
		else
		{
			$offset = $margin + 1;
			foreach($edges[$primary] as $sNode)
				$offset = self::walkTreeInDeep($sNode, $edges, $nodes, $offset, $depth+1, $dontCareEvents);

			$rMargin = $offset;
		}

		// update !
		if($primary != 'ROOT')
		{
			$nodes[$primary]['LEFT_MARGIN'] = intval($lMargin);
			$nodes[$primary]['RIGHT_MARGIN'] = intval($rMargin);
			$nodes[$primary]['DEPTH_LEVEL'] = $depth;
		}

		return $rMargin + 1;
	}

	protected static function applyRestrictions(&$data)
	{
		unset($data['LEFT_MARGIN']);
		unset($data['RIGHT_MARGIN']);
		unset($data['DEPTH_LEVEL']);
	}

	protected static function getNodeInfo($primary)
	{
		$primary = Assert::expectIntegerPositive($primary, '$primary');

		$node = self::getById($primary)->fetch();
		if(!isset($node['ID']))
		{
			throw new Main\SystemException(Loc::getMessage('SALE_LOCATION_TREE_ENTITY_NODE_NOT_FOUND_EXCEPTION'));
		}

		return $node;
	}

	protected static function getMaxMargin()
	{
		$tableName = static::getTableName();

		// todo: write it in orm way
		$res = Main\HttpApplication::getConnection()->query("select A.RIGHT_MARGIN from {$tableName} A order by A.RIGHT_MARGIN desc")->fetch();
		return intval($res['RIGHT_MARGIN']);
	}

	public static function mergeRelationsFromTemporalTable($temporalTabName, $additinalFlds = array(), $fldMap = array())
	{
		$dbConnection = Main\HttpApplication::getConnection();
		$dbHelper = $dbConnection->getSqlHelper();

		$temporalTabName = Assert::expectStringNotNull($temporalTabName, false, 'Name of temporal table must be a non-zero length string');
		$temporalTabName = $dbHelper->forSql($temporalTabName);

		$entityTableName = static::getTableName();

		if(!is_array($additinalFlds))
			$additinalFlds = array();

		$additinalFlds = array_merge(array('LEFT_MARGIN', 'RIGHT_MARGIN', 'DEPTH_LEVEL'), $additinalFlds);

		$fldReplace = array();
		foreach($additinalFlds as &$fld)
		{
			$fld = $dbHelper->forSql($fld);
			$fldReplace[$fld] = is_array($fldMap) && isset($fldMap[$fld]) ? $dbHelper->forSql($fldMap[$fld]) : $fld;
		}

		$idReplace = is_array($fldMap) && isset($fldMap['ID']) ? $dbHelper->forSql($fldMap['ID']) : 'ID';

		if($dbConnection->getType() == 'mysql')
		{
			$sql = 'update '.$entityTableName.', '.$temporalTabName.' set ';
			$additFldCnt = count($additinalFlds);

			for($i = 0; $i < $additFldCnt; $i++)
			{
				$sql .= $entityTableName.'.'.$additinalFlds[$i].' = '.$temporalTabName.'.'.$fldReplace[$additinalFlds[$i]].($i == count($additinalFlds) - 1 ? '' : ', ');
			}

			$sql .= ' where '.$entityTableName.'.ID = '.$temporalTabName.'.'.$idReplace;
		}
		elseif($dbConnection->getType() == 'mssql')
		{
			$sql = 'update '.$entityTableName.' set ';
			$additFldCnt = count($additinalFlds);

			for($i = 0; $i < $additFldCnt; $i++)
			{
				$sql .= $additinalFlds[$i].' = '.$temporalTabName.'.'.$fldReplace[$additinalFlds[$i]].($i == count($additinalFlds) - 1 ? '' : ', ');
			}

			$sql .= ' from '.$entityTableName.' join '.$temporalTabName.' on '.$entityTableName.'.ID = '.$temporalTabName.'.'.$idReplace;
		}
		elseif($dbConnection->getType() == 'oracle')
		{
			// update tab1 set (aa,bb) = (select aa,bb from tab2 where tab2.id = tab1.id)

			$sql = 'update '.$entityTableName.' set ('.
				implode(', ', $additinalFlds).
			') = (select '.
				implode(', ', $fldReplace).
			' from '.$temporalTabName.' where '.$entityTableName.'.ID = '.$temporalTabName.'.'.$idReplace.')';
		}

		$dbConnection->query($sql);
	}

	public static function getCountByFilter($filter = array())
	{
		$params = array(
			'runtime' => array(
				'CNT' => array(
					'data_type' => 'integer',
					'expression' => array('COUNT(*)')
				)
			),
			'select' => array('CNT')
		);

		if(is_array($filter))
			$params['filter'] = $filter;

		$res = static::getList($params)->fetch();

		return intval($res['CNT']);
	}

	protected static function checkNodeThrowException($node)
	{
		// left margin MAY be equal to zero, right margin MAY NOT
		if(!is_numeric($node['LEFT_MARGIN']) || (int) $node['LEFT_MARGIN'] < 0 || !intval($node['RIGHT_MARGIN']) || !intval($node['ID']))
		{
			throw new Tree\NodeIncorrectException(
				false,
				array(
					'INFO' => array(
						'ID' => $node['ID'],
						'CODE' => $node['CODE'],
						'LEFT_MARGIN' => $node['LEFT_MARGIN'],
						'RIGHT_MARGIN' => $node['RIGHT_MARGIN']
			)));
		}
	}

	// deprecated
	protected static function checkNodeIsParentOfNodeByFilters($parentNodeFilter, $nodeFilter, $behaviour = array('CHECK_DIRECT' => false))
	{
		return static::checkNodeIsParentOfNodeByCondition($parentNodeFilter, $nodeFilter, $behaviour);
	}
}