Your IP : 18.117.149.93


Current Path : /home/bitrix/ext_www/klimatlend.ua/bitrix/modules/sale/lib/location/import/
Upload File :
Current File : /home/bitrix/ext_www/klimatlend.ua/bitrix/modules/sale/lib/location/import/importprocess.php

<?
/**
 * 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\Import;

use Bitrix\Main;
use Bitrix\Main\Web\HttpClient;
use Bitrix\Main\IO;

use Bitrix\Sale\Location;
use Bitrix\Sale\Location\DB\BlockInserter;
use Bitrix\Sale\Location\Util\CSVReader;

final class ImportProcess extends Location\Util\Process
{
	const DISTRIBUTOR_HOST = 				'www.1c-bitrix.ru';
	const DISTRIBUTOR_PORT = 				80;
	//const REMOTE_PATH = 					'/locations_data/compiled/';
	const REMOTE_PATH = 					'/download/files/locations/pro/';
	const REMOTE_SETS_PATH = 				'bundles/';
	const REMOTE_LAYOUT_FILE = 				'bundles/layout.csv';
	const REMOTE_TYPE_GROUP_FILE = 			'typegroup.csv';
	const REMOTE_TYPE_FILE = 				'type.v2.csv';
	const REMOTE_EXTERNAL_SERVICE_FILE = 	'externalservice.csv';

	const PACK_STANDARD = 					'standard';
	const PACK_EXTENDED = 					'extended';

	const LOCAL_SETS_PATH = 				'bundles/';
	const LOCAL_LOCATION_FILE = 			'%s.csv';
	const LOCAL_LAYOUT_FILE = 				'layout.csv';
	const LOCAL_TYPE_GROUP_FILE = 			'typegroup.csv';
	const LOCAL_TYPE_FILE = 				'type.csv';
	const LOCAL_EXTERNAL_SERVICE_FILE = 	'externalservice.csv';

	const USER_FILE_DIRECTORY_SESSION_KEY = 'location_import_user_file';
	const USER_FILE_TEMP_NAME = 			'userfile.csv';

	const SOURCE_REMOTE = 					'remote';
	const SOURCE_FILE = 					'file';

	const MAX_CODE_FETCH_BLOCK_LEN = 		90;
	const INSERTER_MTU = 					99999;
	const INSERTER_MTU_ORACLE = 			9999;

	const DB_TYPE_MYSQL = 					'mysql';
	const DB_TYPE_MSSQL = 					'mssql';
	const DB_TYPE_ORACLE = 					'oracle';

	const TREE_REBALANCE_TEMP_BLOCK_LEN = 	99999;
	const TREE_REBALANCE_TEMP_BLOCK_LEN_O = 9999;
	const TREE_REBALANCE_TEMP_TABLE_NAME = 	'b_sale_location_rebalance';

	const DEBUG_MODE = 						false;

	protected $sessionKey = 				'location_import';
	protected $rebalanceInserter = 			false;
	protected $stat = 						array();
	protected $hitData = 					array();
	protected $useCache = 					true;

	protected $dbConnection =				null;
	protected $dbConnType = 				null;
	protected $dbHelper = 					null;

	public function __construct($options)
	{
		if($options['ONLY_DELETE_ALL'])
		{
			$this->addStage(array(
				'PERCENT' => 100,
				'CODE' => 'DELETE_ALL',
				'CALLBACK' => 'stageDeleteAll',
				'SUBPERCENT_CALLBACK' => 'getSubpercentForstageDeleteAll'
			));
		}
		else
		{
			$this->addStage(array(
				'PERCENT' => 5,
				'CODE' => 'DOWNLOAD_FILES',
				'CALLBACK' => 'stageDownloadFiles',
				'SUBPERCENT_CALLBACK' => 'getSubpercentForStageDownloadFiles'
			));

			if($options['REQUEST']['OPTIONS']['DROP_ALL'])
			{
				$this->addStage(array(
					'PERCENT' => 7,
					'CODE' => 'DELETE_ALL',
					'CALLBACK' => 'stageDeleteAll',
					'SUBPERCENT_CALLBACK' => 'getSubpercentForstageDeleteAll'
				));
			}

			$this->addStage(array(
				'PERCENT' => 10,
				'CODE' => 'DROP_INDEXES',
				'CALLBACK' => 'stageDropIndexes',
				'SUBPERCENT_CALLBACK' => 'getSubpercentForStageDropIndexes'
			));

			$this->addStage(array(
				'PERCENT' => 60,
				'STEP_SIZE' => 6000,
				'CODE' => 'PROCESS_FILES',
				'CALLBACK' => 'stageProcessFiles',
				'SUBPERCENT_CALLBACK' => 'getSubpercentForStageProcessFiles'
			));

			if($options['REQUEST']['OPTIONS']['INTEGRITY_PRESERVE'])
			{
				$this->addStage(array(
					'PERCENT' => 65,
					'STEP_SIZE' => 1,
					'CODE' => 'INTEGRITY_PRESERVE',
					'CALLBACK' => 'stageIntegrityPreserve'
				));
			}

			$this->addStage(array(
				'PERCENT' => 90,
				'STEP_SIZE' => 1,
				'CODE' => 'REBALANCE_WALK_TREE',
				'CALLBACK' => 'stageRebalanceWalkTree',
				'SUBPERCENT_CALLBACK' => 'getSubpercentForStageRebalanceWalkTree'
			));

			$this->addStage(array(
				'PERCENT' => 95,
				'STEP_SIZE' => 1,
				'CODE' => 'REBALANCE_CLEANUP_TEMP_TABLE',
				'CALLBACK' => 'stageRebalanceCleanupTempTable'
			));

			$this->addStage(array(
				'PERCENT' => 100,
				'STEP_SIZE' => 1,
				'CODE' => 'RESTORE_INDEXES',
				'CALLBACK' => 'stageRestoreIndexes',
				'SUBPERCENT_CALLBACK' => 'getSubpercentForStageRestoreIndexes'
			));
		}

		$this->dbConnection = Main\HttpApplication::getConnection();
		$this->dbConnType = $this->dbConnection->getType();
		$this->dbHelper = $this->dbConnection->getSqlHelper();

		parent::__construct($options);
	}

	public function onBeforePerformIteration()
	{
		if($this->options['ONLY_DELETE_ALL'])
			return;

		if(!$this->data['inited'])
		{
			if((string) $this->data['LOCAL_PATH'] == '')
			{
				list($this->data['LOCAL_PATH'], $created) = $this->getTemporalDirectory();
			}

			$opts = $this->options['REQUEST']['OPTIONS'];

			if(!in_array($opts['SOURCE'], array(self::SOURCE_REMOTE, self::SOURCE_FILE)))
				throw new Main\SystemException('Unknown import type');

			$sets = array();
			if($opts['SOURCE'] == self::SOURCE_REMOTE)
			{
				$sets = $this->normalizeQueryArray($this->options['REQUEST']['LOCATION_SETS']);
				if(empty($sets))
					throw new Main\SystemException('Nothing to do (no sets selected)');
			}

			$this->data['settings'] = array(
				'sets' => $sets,
				'options' => $opts
			);

			if($opts['SOURCE'] == self::SOURCE_REMOTE)
			{
				$this->data['settings']['additional'] = is_array($this->options['REQUEST']['ADDITIONAL']) ? array_flip(array_values($this->options['REQUEST']['ADDITIONAL'])) : array();

				if(isset($this->data['settings']['additional']['ZIP']))
					$this->data['settings']['additional']['ZIP_LOWER'] = $this->data['settings']['additional']['ZIP'];
			}
			elseif($this->checkSource(self::SOURCE_FILE))
			{
				$this->data['settings']['additional'] = false; // means ANY
			}

			$this->buildTypeTable();
			$this->buildExternalSerivceTable();

			$this->data['inited'] = true;
		}

		if($timeLimit = intval($this->data['settings']['options']['TIME_LIMIT']))
			$this->setTimeLimit($timeLimit);
	}

	/////////////////////////////////////
	// STAGE 1

	protected function stageDownloadFiles()
	{
		if($this->checkSource(self::SOURCE_FILE)) // user uploaded file
		{
			if((string) $_SESSION[static::USER_FILE_DIRECTORY_SESSION_KEY] == '')
				throw new Main\SystemException('User file was not uploaded properly');

			$srcFilePath = $_SESSION[static::USER_FILE_DIRECTORY_SESSION_KEY].'/'.static::USER_FILE_TEMP_NAME;
			$dstFilePath = $this->data['LOCAL_PATH'].self::getFileNameByIndex(0);

			// ensure directory exists
			$this->createDirectory($this->data['LOCAL_PATH'].'/'.self::LOCAL_SETS_PATH);

			if(!@copy($srcFilePath, $dstFilePath))
			{
				$lastError = error_get_last();
				throw new Main\SystemException($lastError['message']);
			}

			$this->data['files'] = array(
				array(
					'size' => filesize($dstFilePath),
					'memgroup' => 'static'
				)
			);

			$this->nextStage();
		}
		elseif($this->checkSource(self::SOURCE_REMOTE)) // get locations from remote server
		{
			if($this->getStep() == 0)
			{
				$this->data['files'] = array();

				$this->cleanWorkDirectory();

				// layout
				$this->determineLayoutToImport();

				// type groups
				$typeGroups = $this->getRemoteTypeGroups();

				// find out what groups we will include
				$this->data['requiredGroups'] = array();
				foreach($typeGroups as $code => $types)
				{
					if($code == 'LAYOUT') // layout is always included
						continue;

					foreach($types as $type)
					{
						if(isset($this->data['types']['allowed'][$type]))
						{
							$this->data['requiredGroups'][] = ToLower($code);
							break;
						}
					}
				}
			}
			else
			{
				$packPath = self::REMOTE_SETS_PATH.($this->data['settings']['options']['PACK'] == self::PACK_EXTENDED ? self::PACK_EXTENDED : self::PACK_STANDARD).'/';
				//$packPath = self::REMOTE_SETS_PATH.'/';

				if($this->getStep() == 1) // get layout (root) file
				{
					$this->data['files'][0] = array(
						'size' => static::downloadFile(self::REMOTE_LAYOUT_FILE, self::getFileNameByIndex(0), false, $this->data['LOCAL_PATH']),
						'onlyThese' => array_flip($this->data['settings']['bundles']['allpoints']),
						'memgroup' => 'static'
					);

					$this->data['fileDownload']['currentEndPoint'] = 0;
					$this->data['fileDownload']['currentFileOffset'] = 1;
				}

				$i =& $this->data['fileDownload']['currentEndPoint'];
				$j =& $this->data['fileDownload']['currentFileOffset'];

				while($this->checkQuota() && isset($this->data['settings']['bundles']['endpoints'][$i])) // process as many bundles as possible
				{
					$ep = $this->data['settings']['bundles']['endpoints'][$i];

					foreach($this->data['requiredGroups'] as $code)
					{
						$name = self::getFileNameByIndex($j);
						$file = $packPath.$ep.'_'.$code.'.csv';
						try
						{
							$this->data['files'][$j] = array(
								'size' => static::downloadFile($file, $name, false, $this->data['LOCAL_PATH']),
								'memgroup' => $ep
							);
							$j++;
						}
						catch(Main\SystemException $e) // 404 or smth - just skip for now
						{
						}
					}
					$i++;
				}

				if(!isset($this->data['settings']['bundles']['endpoints'][$i])) // no more bundles to process, all files downloaded
				{
					unset($this->data['requiredGroups']);
					unset($this->data['settings']['bundles']['endpoints']);

					$this->nextStage();
					return;
				}
			}

			$this->nextStep();
		}
	}

	protected function getSubpercentForStageDownloadFiles()
	{
		$pRange = $this->getCurrentPercentRange();

		$currEp = intval($this->data['fileDownload']['currentEndPoint']);

		if(!$currEp)
			return 0;

		return round($pRange * ($currEp / count($this->data['settings']['bundles']['endpoints'])));
	}

	/////////////////////////////////////
	// STAGE 2

	protected function stageDeleteAll()
	{
		switch($this->step)
		{
			case 0:
				$this->dbConnection->query('truncate table '.Location\LocationTable::getTableName());
				break;
			case 1:
				$this->dbConnection->query('truncate table '.Location\Name\LocationTable::getTableName());
				break;
			case 2:
				$this->dbConnection->query('truncate table '.Location\ExternalTable::getTableName());
				break;
			case 3:
				Location\GroupLocationTable::deleteAll();
				break;
			case 4:
				Location\SiteLocationTable::deleteAll();
				break;
		}

		$this->nextStep();

		if($this->step >= 5)
			$this->nextStage();
	}

	protected function getSubpercentForstageDeleteAll()
	{
		$pRange = $this->getCurrentPercentRange();
		$step = $this->getStep();

		$stepsCount = 5;

		if($step >= $stepsCount)
			return $pRange;
		else
		{
			return round($pRange * ($step / $stepsCount));
		}
	}

	/////////////////////////////////////
	// STAGE 2.5

	protected function stageDropIndexes()
	{
		$indexes = array(
			'IX_B_SALE_LOC_MARGINS',
			'IX_B_SALE_LOC_MARGINS_REV',
			'IX_B_SALE_LOC_PARENT',
			'IX_B_SALE_LOC_DL',
			'IX_B_SALE_LOC_TYPE',
			'IX_B_SALE_LOC_NAME_NAME_U',
			'IX_B_SALE_LOC_NAME_LI_LI',
			'IX_B_SALE_LOC_EXT_LID_SID',

			// old
			'IXS_LOCATION_COUNTRY_ID',
			'IXS_LOCATION_REGION_ID',
			'IXS_LOCATION_CITY_ID',
			'IX_B_SALE_LOCATION_1',
			'IX_B_SALE_LOCATION_2',
			'IX_B_SALE_LOCATION_3'
		);

		if(!isset($indexes[$this->getStep()]))
			$this->nextStage();
		else
		{
			$this->dropIndexes($indexes[$this->getStep()]);
			$this->logMessage('Index dropped: '.$indexes[$this->getStep()]);
			$this->nextStep();
		}
	}

	protected function getSubpercentForStageDropIndexes()
	{
		$pRange = $this->getCurrentPercentRange();
		$step = $this->getStep();

		$indexCount = 14;

		if($step >= $indexCount)
			return $pRange;
		else
		{
			return round($pRange * ($step / $indexCount));
		}
	}

	/////////////////////////////////////
	// STAGE 3

	protected function readBlockFromCurrentFile2()
	{
		$fIndex = 		$this->data['current']['fIndex'];
		$fName = 		self::getFileNameByIndex($fIndex);
		$onlyThese =& 	$this->data['files'][$fIndex]['onlyThese'];

		//$this->logMessage('READ FROM File: '.$fName.' seek to '.$this->data['current']['bytesRead']);

		if(!isset($this->hitData['csv']))
		{
			$file = $this->data['LOCAL_PATH'].$fName;

			if(!file_exists($file) || !is_readable($file))
				throw new Main\SystemException('Cannot open file '.$file.' for reading');

			$this->logMessage('Chargeing File: '.$fName);

			$this->hitData['csv'] = new CSVReader();
			$this->hitData['csv']->LoadFile($file);
			$this->hitData['csv']->AddEventCallback('AFTER_ASSOC_LINE_READ', array($this, 'provideEnFromRu'));
		}

		$block = $this->hitData['csv']->ReadBlockLowLevel($this->data['current']['bytesRead'], 100);

		$this->data['current']['linesRead'] += count($block);

		if(empty($block))
		{
			return array();
		}

		if($this->hitData['csv']->CheckFileIsLegacy())
		{
			$block = self::convertBlock($block);
		}

		if(is_array($onlyThese))
		{
			foreach($block as $i => $line)
			{
				if(is_array($onlyThese) && !isset($onlyThese[$line['CODE']]))
					unset($block[$i]);
			}
		}

		//$this->logMessage('Bytes read: '.$this->data['current']['bytesRead']);

		return $block;
	}

	protected static function checkLocationCodeExists($code)
	{
		if(!strlen($code))
			return false;

		$dbConnection = Main\HttpApplication::getConnection();

		$code = $dbConnection->getSqlHelper()->forSql($code);
		$res = $dbConnection->query("select ID from ".Location\LocationTable::getTableName()." where CODE = '".$code."'")->fetch();

		return $res['ID'];
	}

	protected function importBlock(&$block)
	{
		if(empty($block))
			return;

		$gid = $this->getCurrentGid();

		// here must decide, which languages to import
		$langs = array_flip($this->getRequiredLanguages());

		foreach($block as $i => $data)
		{
			$code = $data['CODE'];

			// this spike is only for cutting off COUNTRY_DISTRICT
			// strongly need for the more generalized mechanism for excluding certain types
			if(!!($this->options['REQUEST']['OPTIONS']['EXCLUDE_COUNTRY_DISTRICT']))
			{
				if(!is_array($this->data['COUNTRY_2_DISTRICT']))
					$this->data['COUNTRY_2_DISTRICT'] = array();

				if($data['TYPE_CODE'] == 'COUNTRY')
				{
					$this->data['LAST_COUNTRY'] = $data['CODE'];
				}
				elseif($data['TYPE_CODE'] == 'COUNTRY_DISTRICT')
				{
					$this->data['COUNTRY_2_DISTRICT'][$code] = $this->data['LAST_COUNTRY'];
					continue;
				}
				else
				{
					if(isset($this->data['COUNTRY_2_DISTRICT'][$data['PARENT_CODE']]))
						$data['PARENT_CODE'] = $this->data['COUNTRY_2_DISTRICT'][$data['PARENT_CODE']];
				}
			}

			// this spike is only for cutting off COUNTRY_DISTRICT
			// strongly need for the more generalized mechanism for excluding certain types
			if(!!($this->options['REQUEST']['OPTIONS']['EXCLUDE_COUNTRY_DISTRICT']))
			{
				if(!is_array($this->data['COUNTRY_2_DISTRICT']))
					$this->data['COUNTRY_2_DISTRICT'] = array();

				if($data['TYPE_CODE'] == 'COUNTRY')
				{
					$this->data['LAST_COUNTRY'] = $data['CODE'];
				}
				elseif($data['TYPE_CODE'] == 'COUNTRY_DISTRICT')
				{
					$this->data['COUNTRY_2_DISTRICT'][$code] = $this->data['LAST_COUNTRY'];
					continue;
				}
				else
				{
					if(isset($this->data['COUNTRY_2_DISTRICT'][$data['PARENT_CODE']]))
						$data['PARENT_CODE'] = $this->data['COUNTRY_2_DISTRICT'][$data['PARENT_CODE']];
				}
			}

			if(isset($this->data['existedlocs']['static'][$code]) || isset($this->data['existedlocs'][$gid][$code])) // already exists
				continue;

			if(!isset($this->data['types']['allowed'][$data['TYPE_CODE']])) // disallowed
				continue;

			// have to check existence first
			if(!$this->data['TABLE_WERE_EMPTY'])
			{
				$existedId = $this->checkLocationCodeExists($code);

				if(intval($existedId))
				{
					$this->data['existedlocs'][$gid][$code] = $existedId;
					continue;
				}
			}

			///////////////////////////////////////////
			// transform parent
			if(strlen($data['PARENT_CODE']))
			{
				if(isset($this->data['existedlocs']['static'][$data['PARENT_CODE']]))
				{
					$data['PARENT_ID'] = $this->data['existedlocs']['static'][$data['PARENT_CODE']];
				}
				elseif(isset($this->data['existedlocs'][$gid][$data['PARENT_CODE']]))
				{
					$data['PARENT_ID'] = $this->data['existedlocs'][$gid][$data['PARENT_CODE']];
				}
				else
					$data['PARENT_ID'] = 0;
			}
			else
				$data['PARENT_ID'] = 0;

			unset($data['PARENT_CODE']);

			///////////////////////////////////////////
			// transform type
			$data['TYPE_ID'] = $this->data['types']['code2id'][$data['TYPE_CODE']];
			unset($data['TYPE_CODE']);

			///////////////////////////////////////////
			// add
			$names = $data['NAME'];
			unset($data['NAME']);

			$external = $data['EXT'];
			unset($data['EXT']);

			$data['LONGITUDE'] = floatval($data['LONGITUDE']);
			$data['LATITUDE'] = floatval($data['LATITUDE']);
			if(!$this->checkExternalServiceAllowed('GEODATA'))
			{
				$data['LONGITUDE'] = 0;
				$data['LATITUDE'] = 0;
			}

			$locationId = $this->hitData['HANDLES']['LOCATION']->insert($data);

			// store for further PARENT_CODE to PARENT_ID mapping
			//if(!strlen($this->data['types']['last']) || $this->data['types']['last'] != $data['TYPE_CODE'])
				$this->data['existedlocs'][$gid][$data['CODE']] = $locationId;

			///////////////////////////////////////////
			// add names
			if(is_array($names) && !empty($names))
			{
				if(is_array($langs))
				{
					foreach($langs as $lid => $f)
					{
						$lid = ToLower($lid);
						$toAdd = static::getTranslatedName($names, $lid);

						$this->hitData['HANDLES']['NAME']->insert(array(
							'NAME' => $toAdd['NAME'],
							'NAME_UPPER' => ToUpper($toAdd['NAME']),
							'LANGUAGE_ID' => $lid,
							'LOCATION_ID' => $locationId
						));
					}
				}
			}

			///////////////////////////////////////////
			// add external
			if(is_array($external) && !empty($external))
			{
				foreach($external as $sCode => $values)
				{
					if($this->checkExternalServiceAllowed($sCode))
					{
						$serviceId = $this->data['externalService']['code2id'][$sCode];

						if(!$serviceId)
							throw new Main\SystemException('Location import failed: external service doesnt exist');

						if($sCode == 'ZIP_LOWER')
						{
							if(strlen($values) <= 0)
								continue;

							$values = explode(',', $values);

							if(!is_array($values))
								continue;

							$values = array_unique($values);
						}

						if(is_array($values))
						{
							foreach($values as $val)
							{
								if(strlen($val) <= 0)
									continue;

								$this->hitData['HANDLES']['EXTERNAL']->insert(array(
									'SERVICE_ID' => 	$serviceId,
									'XML_ID' => 		$val,
									'LOCATION_ID' => 	$locationId
								));
							}
						}
					}
				}
			}
		}
	}

	protected function getCurrentGid()
	{
		return $this->data['files'][$this->data['current']['fIndex']]['memgroup'];
	}

	protected function stageProcessFiles()
	{
		if($this->dbConnType == self::DB_TYPE_ORACLE)
			$mtu = self::INSERTER_MTU_ORACLE;
		else
			$mtu = self::INSERTER_MTU;

		$this->hitData['HANDLES']['LOCATION'] = new BlockInserter(array(
			'entityName' => '\Bitrix\Sale\Location\LocationTable',
			'exactFields' => array('CODE', 'TYPE_ID', 'PARENT_ID', 'LATITUDE', 'LONGITUDE'),
			'parameters' => array(
				'autoIncrementFld' => 'ID',
				'mtu' => $mtu
			)
		));

		$this->hitData['HANDLES']['NAME'] = new BlockInserter(array(
			'entityName' => '\Bitrix\Sale\Location\Name\LocationTable',
			'exactFields' => array('NAME', 'NAME_UPPER', 'LANGUAGE_ID', 'LOCATION_ID'),
			'parameters' => array(
				'mtu' => $mtu
			)
		));

		$this->hitData['HANDLES']['EXTERNAL'] = new BlockInserter(array(
			'entityName' => '\Bitrix\Sale\Location\ExternalTable',
			'exactFields' => array('SERVICE_ID', 'XML_ID', 'LOCATION_ID'),
			'parameters' => array(
				'mtu' => $mtu
			)
		));

		if($this->getStep() == 0)
		{
			// set initial values
			$this->data['current'] = array(
				'fIndex' => 0,
				'bytesRead' => 0, // current file bytes read
				'linesRead' => 0
			);

			$this->hitData['HANDLES']['LOCATION']->resetAutoIncrementFromIndex(); // synchronize sequences, etc...

			// check if we are empty
			$this->data['TABLE_WERE_EMPTY'] = Location\LocationTable::getCountByFilter() == 0;

			$this->buildStaticLocationIndex();
		}

		while($this->checkQuota())
		{
			$block = $this->readBlockFromCurrentFile2();
			$this->importBlock($block);

			// clean memory
			$this->manageExistedLocationIndex(array($this->getCurrentGid()));

			// or the current file is completely exhausted
			if($this->checkFileCompletelyRead())
			{
				//$this->logMessage('Lines read: '.$this->data['current']['linesRead']);

				// charge next file
				unset($this->hitData['csv']);
				$this->data['current']['fIndex']++; // next file to go
				$this->data['current']['bytesRead'] = 0; // read counter from the beginning
				$this->data['current']['linesRead'] = 0;
				$this->data['current']['legacy'] = array(); // drop legacy data of the file, if were any. bye-bye

				// may be that is all?
				if($this->checkAllFilesRead())
				{
					unset($this->data['existedlocs']); // uff, remove that huge array at last

					$this->nextStage();
					break;
				}
			}

			$this->nextStep();
		}

		$this->hitData['HANDLES']['LOCATION']->flush();
		$this->hitData['HANDLES']['NAME']->flush();
		$this->hitData['HANDLES']['EXTERNAL']->flush();

		$this->logMessage('Inserted, go next: '.$this->getHitTimeString());

		$this->logMemoryUsage();
	}

	protected function getSubpercentForStageProcessFiles()
	{
		$pRange = $this->getStagePercent($this->stage) - $this->getStagePercent($this->stage - 1);

		$totalSize = 0;
		$fileBytesRead = 0;

		if(!isset($this->data['current']['fIndex']))
			return 0;

		$fIndex = $this->data['current']['fIndex'];

		$i = -1;
		foreach($this->data['files'] as $file)
		{
			$i++;

			if($i < $fIndex)
				$fileBytesRead += $file['size'];

			$totalSize += $file['size'];
		}

		if(!$totalSize)
			return 0;

		return round($pRange * (intval($fileBytesRead + $this->data['current']['bytesRead']) / $totalSize));
	}

	/////////////////////////////////////
	// STAGE 4

	protected function stageIntegrityPreserve()
	{
		$lay = $this->getRemoteLayout(true);

		$this->restoreIndexes('IX_B_SALE_LOC_PARENT');

		$res = Location\LocationTable::getList(array(
			'select' => array(
				'ID', 'CODE'
			),
			'filter' => array(
				'=PARENT_ID' => 0
			)
		));
		$relations = array();
		$code2id = array();
		while($item = $res->fetch())
		{
			if(isset($lay[$item['CODE']]) && ((string) $lay[$item['CODE']]['PARENT_CODE'] != '')/*except root*/)
				$relations[$item['CODE']] = $lay[$item['CODE']]['PARENT_CODE'];
			// relations is a match between codes from the layout file

			$code2id[$item['CODE']] = $item['ID'];
		}

		$parentCode2id = $this->getLocationCodeToIdMap($relations);

		foreach($code2id as $code => $id)
		{
			if(isset($parentCode2id[$relations[$code]]) && ((string) $parentCode2id[$relations[$code]] != '')) // parent really exists
			{
				$res = Location\LocationTable::update($id, array('PARENT_ID' => $parentCode2id[$relations[$code]]));
				if(!$res->isSuccess())
					throw new Main\SystemException('Cannot make element become a child of its legal parent');
			}
		}

		$this->nextStage();
	}

	/////////////////////////////////////
	// STAGE 5

	protected function stageRebalanceWalkTree()
	{
		if(!isset($this->data['rebalance']['queue']))
		{
			$this->restoreIndexes('IX_B_SALE_LOC_PARENT');

			$this->logMessage('initialize Queue');

			$this->data['rebalance']['margin'] = -1;
			$this->data['processed'] = 0;
			$this->data['rebalance']['queue'] = array(array('I' => 'root', 'D' => 0));

			$tableName = Location\LocationTable::getTableName();
			$res = Main\HttpApplication::getConnection()->query("select count(*) as CNT from {$tableName}")->fetch();

			$this->data['rebalance']['cnt'] = intval($res['CNT']);
		}

		$i = -1;
		while(!empty($this->data['rebalance']['queue']) && $this->checkQuota())
		{
			$i++;

			$node =& $this->data['rebalance']['queue'][0];

			if(isset($node['L']))
			{
				// we have already been here
				array_shift($this->data['rebalance']['queue']);
				if($node['I'] != 'root') // we dont need for ROOT item in outgoing
				{
					$this->acceptRebalancedNode(array(
						'I' => $node['I'],
						'D' => $node['D'],
						'L' => $node['L'],
						'R' => ++$this->data['rebalance']['margin']
					));
				}
				else
					$this->data['rebalance']['margin']++;
			}
			else
			{
				$a = $this->getCachedBundle($node['I']);

				if(!empty($a))
				{
					// go deeper
					$node['L'] = ++$this->data['rebalance']['margin'];

					foreach($a as $id)
					{
						if($this->checkNodeIsParent($id))
						{
							array_unshift($this->data['rebalance']['queue'], array('I' => $id, 'D' => $node['D'] + 1));
						}
						else // we dont need to put it to the query
						{
							$this->acceptRebalancedNode(array(
								'I' => $id,
								'D' => $node['D'] + 1,
								'L' => ++$this->data['rebalance']['margin'],
								'R' => ++$this->data['rebalance']['margin']
							));
						}
					}
				}
				else
				{
					array_shift($this->data['rebalance']['queue']);
					$this->acceptRebalancedNode(array(
						'I' => $node['I'],
						'D' => $node['D'],
						'L' => ++$this->data['rebalance']['margin'],
						'R' => ++$this->data['rebalance']['margin']
					));
				}
			}
		}

		$this->logMessage('Q size is '.count($this->data['rebalance']['queue']).' already processed: '.$this->data['processed'].'/'.$this->data['rebalance']['cnt']);
		$this->logMemoryUsage();

		if(empty($this->data['rebalance']['queue']))
		{
			// last flush & then merge
			$this->mergeRebalancedNodes();

			$this->nextStage();
			return;
		}

		if($this->rebalanceInserter)
			$this->rebalanceInserter->flush();

		$this->nextStep();
	}

	protected function getSubpercentForStageRebalanceWalkTree()
	{
		if(!$this->data['processed'] || !$this->data['rebalance']['cnt'])
			return 0;

		$pRange = $this->getCurrentPercentRange();
		$part = round($pRange * ($this->data['processed'] / $this->data['rebalance']['cnt']));

		return $part >= $pRange ? $pRange : $part;
	}

	/////////////////////////////////////
	// STAGE 6

	protected function stageRebalanceCleanupTempTable()
	{
		$this->dropTempTable();
		$this->nextStage();
	}

	/////////////////////////////////////
	// STAGE 7

	protected function stageRestoreIndexes()
	{
		$indexes = array(
			'IX_B_SALE_LOC_MARGINS',
			'IX_B_SALE_LOC_MARGINS_REV',
			//'IX_B_SALE_LOC_PARENT', // already restored at REBALANCE_WALK_TREE stage
			'IX_B_SALE_LOC_DL',
			'IX_B_SALE_LOC_TYPE',
			'IX_B_SALE_LOC_NAME_NAME_U',
			'IX_B_SALE_LOC_NAME_LI_LI',
			'IX_B_SALE_LOC_EXT_LID_SID',

			// legacy
			'IXS_LOCATION_COUNTRY_ID',
			'IXS_LOCATION_REGION_ID',
			'IXS_LOCATION_CITY_ID',
		);

		if(isset($indexes[$this->getStep()]))
		{
			$this->restoreIndexes($indexes[$this->getStep()]);
			$this->logMessage('Index restored: '.$indexes[$this->getStep()]);
			$this->nextStep();
		}
		else
		{
			Location\LocationTable::resetLegacyPath(); // for backward compatibility
			$this->nextStage();
		}
	}

	protected function getSubpercentForStageRestoreIndexes()
	{
		$pRange = $this->getCurrentPercentRange();
		$step = $this->getStep();

		$stepCount = 12;

		if($step >= $stepCount)
		{
			return $pRange;
		}
		else
		{
			return round($pRange * ($step / $stepCount));
		}
	}

	/////////////////////////////////////
	// about stage util functions

	protected function getLanguageId()
	{
		if(isset($this->options['LANGUAGE_ID']))
			return $this->options['LANGUAGE_ID'];

		return LANGUAGE_ID;
	}

	/**
	 * @deprecated
	 */
	public function getTypes()
	{
		$result = array();
		$res = Location\TypeTable::getList(array(
			'select' => array(
				'CODE', 'TNAME' => 'NAME.NAME'
			),
			'filter' => array(
				'NAME.LANGUAGE_ID' => $this->getLanguageId()
			),
			'order' => array(
				'SORT' => 'asc',
				'NAME.NAME' => 'asc'
			)
		));
		while($item = $res->fetch())
			$result[$item['CODE']] = $item['TNAME'];

		return $result;
	}

	public function getStatisticsAll()
	{
		$this->getStatistics();

		return $this->stat;
	}

	public function getStatistics($type = 'TOTAL')
	{
		if(empty($this->stat))
		{
			$types = \Bitrix\Sale\Location\Admin\TypeHelper::getTypes(array('LANGUAGE_ID' => $this->getLanguageId()));

			$res = Location\LocationTable::getList(array(
				'select' => array(
					'CNT',
					'TCODE' => 'TYPE.CODE'
				),
				'group' => array(
					'TYPE_ID'
				)
			));
			$total = 0;
			$stat = array();
			while($item = $res->fetch())
			{
				$total += intval($item['CNT']);
				$stat[$item['TCODE']] = $item['CNT'];
			}

			foreach($types as $code => $data)
			{
				$this->stat[$code] = array(
					'NAME' => $data['NAME_CURRENT'],
					'CODE' => $code,
					'CNT' => isset($stat[$code]) ? intval($stat[$code]) : 0,
				);
			}

			$this->stat['TOTAL'] = array('CNT' => $total, 'CODE' => 'TOTAL');

			$res = Location\GroupTable::getList(array(
				'runtime' => array(
					'CNT' => array(
						'data_type' => 'integer',
						'expression' => array(
							'COUNT(*)'
						)
					)
				),
				'select' => array(
					'CNT'
				)
			))->fetch();

			$this->stat['GROUPS'] = array('CNT' => intval($res['CNT']), 'CODE' => 'GROUPS');
		}

		return intval($this->stat[$type]['CNT']);
	}

	public function determineLayoutToImport()
	{
		$lay = $this->getRemoteLayout(true);

		$parentness = array();
		foreach($lay as $data)
			$parentness[$data['PARENT_CODE']] += 1;

		$bundles = array_flip($this->data['settings']['sets']);

		$selectedLayoutParts = array();
		foreach($bundles as $bundle => $void)
		{
			if(!isset($lay[$bundle]))
				throw new Main\SystemException('Unknown bundle passed in request');

			// obtaining intermediate chain parts
			$chain = array();

			$currentBundle = $bundle;
			$i = -1;
			while($currentBundle)
			{
				$i++;

				if($i > 50) // smth is really bad
					throw new Main\SystemException('Too deep recursion got when building chains. Layout file is broken');

				if(isset($lay[$currentBundle]))
				{
					$chain[] = $currentBundle;
					if(strlen($lay[$currentBundle]['PARENT_CODE']))
					{
						$currentBundle = $lay[$currentBundle]['PARENT_CODE'];

						if(!isset($lay[$currentBundle]))
							throw new Main\SystemException('Unknown parent bundle found ('.$currentBundle.'). Layout file is broken');
					}
					else
						$currentBundle = false;
				}
			}

			if(is_array($chain) && !empty($chain))
			{
				$chain = array_reverse($chain);

				// find first occurance of selected bundle in the chain
				$subChain = array();
				foreach($chain as $i => $node)
				{
					if(isset($bundles[$node]))
					{
						$subChain = array_slice($chain, $i);
						break;
					}
				}

				if(!empty($subChain))
					$selectedLayoutParts = array_merge($selectedLayoutParts, $subChain);
			}
		}

		//$this->data['settings']['layout'] = $lay;
		$selectedLayoutParts = array_unique($selectedLayoutParts);

		$this->data['settings']['bundles'] = array('endpoints' => array(), 'allpoints' => $selectedLayoutParts);

		foreach($selectedLayoutParts as $bCode)
		{
			if(!isset($parentness[$bCode]))
				$this->data['settings']['bundles']['endpoints'][] = $bCode;
			//else
			//	$this->data['settings']['bundles']['middlepoints'][] = $bCode;
		}
		unset($this->data['settings']['sets']);
	}

	public function convertBlock($block)
	{
		$converted = array();

		foreach($block as $line)
		{
			if($line[0] == 'S')
				$typeCode = 'COUNTRY';
			elseif($line[0] == 'R')
				$typeCode = 'REGION';
			elseif($line[0] == 'T')
				$typeCode = 'CITY';
			else
				throw new Main\SystemException('Unknown type found in legacy file');

			$code = md5(implode(':', $line));

			if($typeCode == 'REGION')
				$parentCode = $this->data['current']['legacy']['lastCOUNTRY'];
			elseif($typeCode == 'CITY')
				$parentCode = $this->data['current']['legacy']['lastParent'];
			else
				$parentCode = '';

			if($typeCode != 'CITY')
			{
				$this->data['current']['legacy']['last'.$typeCode] = $code;
				$this->data['current']['legacy']['lastParent'] = $code;
			}

			$cLine = array(
				'CODE' => $code,
				'TYPE_CODE' => $typeCode,
				'PARENT_CODE' => $parentCode
			);

			$lang = false;
			$expectLang = true;
			$lineLen = count($line);
			for($k = 1; $k < $lineLen; $k++)
			{
				if($expectLang)
				{
					$lang = $line[$k];
				}
				else
				{
					$cLine['NAME'][ToUpper($lang)]['NAME'] = $line[$k];
				}

				$expectLang = !$expectLang;
			}

			$converted[] = $cLine;
		}

		return $converted;
	}

	public function checkSource($sType)
	{
		return $this->data['settings']['options']['SOURCE'] == $sType;
	}

	// download layout from server
	public function getRemoteLayout($getFlat = false)
	{
		list($localPath, $tmpDirCreated) = $this->getTemporalDirectory();

		static::downloadFile(self::REMOTE_LAYOUT_FILE, self::LOCAL_LAYOUT_FILE, false, $localPath);

		$csv = new CSVReader();
		$csv->AddEventCallback('AFTER_ASSOC_LINE_READ', array($this, 'provideEnFromRu'));
		$res = $csv->ReadBlock($localPath.self::LOCAL_LAYOUT_FILE);

		$result = array();
		if($getFlat)
		{
			foreach($res as $line)
				$result[$line['CODE']] = $line;
			$csv->CloseFile();
			return $result;
		}

		$lang = $this->getLanguageId();

		foreach($res as $line)
		{
			$line['NAME'][ToUpper($lang)] = static::getTranslatedName($line['NAME'], $lang);
			$result[$line['PARENT_CODE']][$line['CODE']] = $line;
		}
		$csv->CloseFile();

		if($tmpDirCreated)
		{
			$this->deleteDirectory($localPath);
		}

		return $result;
	}

	// download types from server
	public function getRemoteTypes()
	{
		if(!$this->useCache || !isset($this->data['settings']['remote']['types']))
		{
			list($localPath, $tmpDirCreated) = $this->getTemporalDirectory();

			static::downloadFile(self::REMOTE_TYPE_FILE, self::LOCAL_TYPE_FILE, false, $localPath);

			$csv = new CSVReader();
			$csv->AddEventCallback('AFTER_ASSOC_LINE_READ', array($this, 'provideEnFromRu'));
			$res = $csv->ReadBlock($localPath.self::LOCAL_TYPE_FILE);

			$result = array();
			foreach($res as $line)
				$result[$line['CODE']] = $line;

			$this->data['settings']['remote']['types'] = $result;
			$csv->CloseFile();
			if($tmpDirCreated)
			{
				$this->deleteDirectory($localPath);
			}
		}

		return $this->data['settings']['remote']['types'];
	}

	// download external services from server
	public function getRemoteExternalServices()
	{
		if(!$this->useCache || !isset($this->data['settings']['remote']['external_services']))
		{
			list($localPath, $tmpDirCreated) = $this->getTemporalDirectory();

			static::downloadFile(self::REMOTE_EXTERNAL_SERVICE_FILE, self::LOCAL_EXTERNAL_SERVICE_FILE, false, $localPath);

			$csv = new CSVReader();
			$res = $csv->ReadBlock($localPath.self::LOCAL_EXTERNAL_SERVICE_FILE);

			$result = array();
			foreach($res as $line)
				$result[$line['CODE']] = $line;

			$this->data['settings']['remote']['external_services'] = $result;
			$csv->CloseFile();
			if($tmpDirCreated)
			{
				$this->deleteDirectory($localPath);
			}
		}

		return $this->data['settings']['remote']['external_services'];
	}

	// download type groups from server
	public function getRemoteTypeGroups()
	{
		if(!$this->useCache || !isset($this->data['settings']['remote']['typeGroups']))
		{
			list($localPath, $tmpDirCreated) = $this->getTemporalDirectory();

			static::downloadFile(self::REMOTE_TYPE_GROUP_FILE, self::LOCAL_TYPE_GROUP_FILE, false, $localPath);

			$csv = new CSVReader();
			$res = $csv->ReadBlock($localPath.self::LOCAL_TYPE_GROUP_FILE);

			$result = array();
			foreach($res as $line)
			{
				$result[$line['CODE']] = explode(':', $line['TYPES']);
			}

			$this->data['settings']['remote']['typeGroups'] = $result;
			$csv->CloseFile();
			if($tmpDirCreated)
			{
				$this->deleteDirectory($localPath);
			}
		}

		return $this->data['settings']['remote']['typeGroups'];
	}

	public function getTypeLevels($langId = LANGUAGE_ID)
	{
		$types = $this->getRemoteTypes();
		$levels = array();

		if(!isset($langId))
			$langId = $this->getLanguageId();

		foreach($types as $type)
		{
			if($type['SELECTORLEVEL'] = intval($type['SELECTORLEVEL']))
			{
				$name = static::getTranslatedName($type['NAME'], $langId);
				$levels[$type['SELECTORLEVEL']]['NAMES'][] = $name['NAME'];
				$levels[$type['SELECTORLEVEL']]['TYPES'][] = $type['CODE'];

				if($type['DEFAULTSELECT'] == '1')
					$levels[$type['SELECTORLEVEL']]['DEFAULT'] = true;
			}
		}

		foreach($levels as &$group)
			$group['NAMES'] = implode(', ', $group['NAMES']);

		ksort($levels, SORT_NUMERIC);

		return $levels;
	}

	public static function getSiteLanguages()
	{
		static $langs;

		if($langs == null)
		{
			$langs = array();

			$res = \Bitrix\Main\SiteTable::getList(array('filter' => array('ACTIVE' => 'Y'), 'select' => array('LANGUAGE_ID'), 'group' => array('LANGUAGE_ID')));
			while($item = $res->fetch())
			{
				$langs[ToUpper($item['LANGUAGE_ID'])] = true;
			}

			$langs = array_unique(array_keys($langs)); // all active sites languages
		}

		return $langs;
	}

	public function getRequiredLanguages()
	{
		$required = array(ToUpper($this->getLanguageId()));

		$langs = Location\Admin\NameHelper::getLanguageList();
		if(isset($langs['en']))
			$required[] = 'EN';

		return array_unique(array_merge($required, static::getSiteLanguages())); // current language plus for all active sites
	}

	// read type.csv and build type table
	protected function buildTypeTable()
	{
		if($this->data['types_processed'])
			return;

		// read existed
		$existed = static::getExistedTypes();

		if($this->checkSource(self::SOURCE_REMOTE))
		{
			$rTypes = $this->getRemoteTypes();
			$this->getRemoteTypeGroups();

			$existed = static::createTypes($rTypes, $existed);

			if(intval($dl = $this->data['settings']['options']['DEPTH_LIMIT']))
			{
				// here we must find out what types we are allowed to read

				$typesGroupped = $this->getTypeLevels();

				if(!isset($typesGroupped[$dl]))
					throw new Main\SystemException('Unknow type level to limit');

				$allowed = array();
				foreach($typesGroupped as $gId => $group)
				{
					if($gId > $dl)
						break;

					foreach($group['TYPES'] as $type)
						$allowed[] = $type;
				}

				$this->data['types']['allowed'] = $allowed;
			}
			else
			{
				foreach($rTypes as $type)
					$this->data['types']['allowed'][] = $type['CODE'];
			}
		}
		elseif($this->checkSource(self::SOURCE_FILE))
		{
			$codes = array();
			if(is_array($existed) && !empty($existed))
				$codes = array_keys($existed);

			$this->data['types']['allowed'] = $codes;
		}

		$this->data['types']['last'] = $this->data['types']['allowed'][count($this->data['types']['allowed']) - 1];
		$this->data['types']['allowed'] = array_flip($this->data['types']['allowed']);

		$this->data['types']['code2id'] = $existed;
		$this->data['types_processed'] = true;
	}

	protected function checkExternalServiceAllowed($code)
	{
		if($this->data['settings']['additional'] === false)
			return true; // ANY

		if($code == 'ZIP_LOWER')
			$code = 'ZIP';

		return isset($this->data['settings']['additional'][$code]);
	}

	protected function buildExternalSerivceTable()
	{
		if($this->data['external_processed'])
			return;

		// read existed
		$existed = static::getExistedServices();

		if($this->checkSource(self::SOURCE_REMOTE))
		{
			$external = $this->getRemoteExternalServices();
			foreach($external as $line)
			{
				if(!isset($existed[$line['CODE']]) && $this->checkExternalServiceAllowed($line['CODE']))
				{
					$existed[$line['CODE']] = static::createService($line);
				}
			}
			unset($this->data['settings']['remote']['external_services']);
		}

		$this->data['externalService']['code2id'] = $existed;
		$this->data['external_processed'] = true;
	}

	protected function buildStaticLocationIndex()
	{
		$parameters = array(
			'select' => array('ID', 'CODE')
		);

		// get static index, it will be always in memory
		$parameters['filter'] = array('TYPE.CODE' => array('COUNTRY', 'COUNTRY_DISTRICT', 'REGION')); // todo: from typegroup later

		$this->data['existedlocs'] = array('static' => array());
		$res = Location\LocationTable::getList($parameters);
		while($item = $res->fetch())
			$this->data['existedlocs']['static'][$item['CODE']] = $item['ID']; // get existed, "static" index
	}

	protected function getLocationCodeToIdMapQuery($buffer, &$result)
	{
		$res = Location\LocationTable::getList(array('filter' => array('CODE' => $buffer), 'select' => array('ID', 'CODE')));
		while($item = $res->fetch())
			$result[$item['CODE']] = $item['ID'];
	}

	protected function getLocationCodeToIdMap($codes)
	{
		if(empty($codes))
			return array();

		$i = -1;
		$buffer = array();
		$result = array();
		foreach($codes as $code)
		{
			$i++;

			if($i == self::MAX_CODE_FETCH_BLOCK_LEN)
			{
				$this->getLocationCodeToIdMapQuery($buffer, $result);

				$buffer = array();
				$i = -1;
			}

			if(strlen($code))
				$buffer[] = $code;
		}

		// last iteration
		$this->getLocationCodeToIdMapQuery($buffer, $result);

		return $result;
	}

	protected function manageExistedLocationIndex($memGroups)
	{
		$before = implode(', ', array_keys($this->data['existedlocs']));

		$cleaned = false;
		foreach($this->data['existedlocs'] as $gid => $bundles)
		{
			if($gid == 'static' || in_array($gid, $memGroups))
				continue;

			$cleaned = true;

			$this->logMessage('Memory clean: REMOVING Group '.$gid);
			unset($this->data['existedlocs'][$gid]);
		}

		if($cleaned)
		{
			$this->logMessage('BEFORE memgroups: '.$before);
			$this->logMessage('Clear all but '.$memGroups[0]);

			$this->logMessage('AFTER memgroups: '.implode(', ', array_keys($this->data['existedlocs'])));
		}
	}

	/////////////////////////////////////
	// about file and network I/O

	protected function checkIndexExistsByName($indexName, $tableName)
	{
		$indexName = $this->dbHelper->forSql(trim($indexName));
		$tableName = $this->dbHelper->forSql(trim($tableName));

		if(!strlen($indexName) || !strlen($tableName))
			return false;

		if($this->dbConnType == self::DB_TYPE_MYSQL)
			$res = $this->dbConnection->query("show index from ".$tableName);
		elseif($this->dbConnType == self::DB_TYPE_ORACLE)
			$res = $this->dbConnection->query("SELECT INDEX_NAME as Key_name FROM USER_IND_COLUMNS WHERE TABLE_NAME = '".ToUpper($tableName)."'");
		elseif($this->dbConnType == self::DB_TYPE_MSSQL)
		{
			$res = $this->dbConnection->query("SELECT si.name Key_name
				FROM sysindexkeys s
					INNER JOIN syscolumns c ON s.id = c.id AND s.colid = c.colid
					INNER JOIN sysobjects o ON s.id = o.Id AND o.xtype = 'U'
					LEFT JOIN sysindexes si ON si.indid = s.indid AND si.id = s.id
				WHERE o.name = '".ToUpper($tableName)."'");
		}

		while($item = $res->fetch())
		{
			if($item['Key_name'] == $indexName || $item['KEY_NAME'] == $indexName)
				return true;
		}

		return false;
	}

	protected function dropIndexByName($indexName, $tableName)
	{
		$indexName = $this->dbHelper->forSql(trim($indexName));
		$tableName = $this->dbHelper->forSql(trim($tableName));

		if(!strlen($indexName) || !strlen($tableName))
			return false;

		if(!$this->checkIndexExistsByName($indexName, $tableName))
			return false;

		if($this->dbConnType == self::DB_TYPE_MYSQL)
			$this->dbConnection->query("alter table {$tableName} drop index {$indexName}");
		elseif($this->dbConnType == self::DB_TYPE_ORACLE)
			$this->dbConnection->query("drop index {$indexName}");
		elseif($this->dbConnType == self::DB_TYPE_MSSQL)
			$this->dbConnection->query("drop index {$indexName} on {$tableName}");

		return true;
	}

	public static function getIndexMap()
	{
		$locationTable = Location\LocationTable::getTableName();
		$locationNameTable = Location\Name\LocationTable::getTableName();
		$locationExternalTable = Location\ExternalTable::getTableName();

		return array(
			'IX_B_SALE_LOC_MARGINS' => array('TABLE' => $locationTable, 'COLUMNS' => array('LEFT_MARGIN', 'RIGHT_MARGIN')),
			'IX_B_SALE_LOC_MARGINS_REV' => array('TABLE' => $locationTable, 'COLUMNS' => array('RIGHT_MARGIN', 'LEFT_MARGIN')),
			'IX_B_SALE_LOC_PARENT' => array('TABLE' => $locationTable, 'COLUMNS' => array('PARENT_ID')),
			'IX_B_SALE_LOC_DL' => array('TABLE' => $locationTable, 'COLUMNS' => array('DEPTH_LEVEL')),
			'IX_B_SALE_LOC_TYPE' => array('TABLE' => $locationTable, 'COLUMNS' => array('TYPE_ID')),
			'IX_B_SALE_LOC_NAME_NAME_U' => array('TABLE' => $locationNameTable, 'COLUMNS' => array('NAME_UPPER')),
			'IX_B_SALE_LOC_NAME_LI_LI' => array('TABLE' => $locationNameTable, 'COLUMNS' => array('LOCATION_ID', 'LANGUAGE_ID')),
			'IX_B_SALE_LOC_EXT_LID_SID' => array('TABLE' => $locationExternalTable, 'COLUMNS' => array('LOCATION_ID', 'SERVICE_ID')),

			// legacy
			'IXS_LOCATION_COUNTRY_ID' => array('TABLE' => $locationTable, 'COLUMNS' => array('COUNTRY_ID')),
			'IXS_LOCATION_REGION_ID' => array('TABLE' => $locationTable, 'COLUMNS' => array('REGION_ID')),
			'IXS_LOCATION_CITY_ID' => array('TABLE' => $locationTable, 'COLUMNS' => array('CITY_ID')),

			// obsolete
			'IX_B_SALE_LOCATION_1' => array('TABLE' => $locationTable, 'COLUMNS' => array('COUNTRY_ID'), 'DROP_ONLY' => true),
			'IX_B_SALE_LOCATION_2' => array('TABLE' => $locationTable, 'COLUMNS' => array('REGION_ID'), 'DROP_ONLY' => true),
			'IX_B_SALE_LOCATION_3' => array('TABLE' => $locationTable, 'COLUMNS' => array('CITY_ID'), 'DROP_ONLY' => true),
		);
	}

	protected function dropIndexes($certainIndex = false)
	{
		$map = static::getIndexMap();

		foreach($map as $index => $ixData)
		{
			if($certainIndex !== false && $certainIndex != $index)
				continue;

			$this->dropIndexByName($index, $ixData['TABLE']);
		}
	}

	public function restoreIndexes($certainIndex = false)
	{
		$map = $this->getIndexMap();

		foreach($map as $ixName => $ixData)
		{
			if($ixData['DROP_ONLY'] === true)
				continue;

			if($certainIndex !== false && $certainIndex != $ixName)
				continue;

			if($this->checkIndexExistsByName($ixName, $ixData['TABLE']))
				return false;

			$this->dbConnection->query('CREATE INDEX '.$ixName.' ON '.$ixData['TABLE'].' ('.implode(', ', $ixData['COLUMNS']).')');
		}
	}

	private function getCachedBundle($id)
	{
		$locationTable = Location\LocationTable::getTableName();

		$bundle = array();
		$res = $this->dbConnection->query("select ID from {$locationTable} where PARENT_ID = ".($id == 'root' ? '0' : intval($id)));
		while($item = $res->fetch())
			$bundle[] = $item['ID'];

		return $bundle;
	}

	private function checkNodeIsParent($id)
	{
		$locationTable = Location\LocationTable::getTableName();

		$res = $this->dbConnection->query("select count(*) as CNT from {$locationTable} where PARENT_ID = ".($id == 'root' ? '0' : intval($id)))->fetch();

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

	private function mergeRebalancedNodes()
	{
		if($this->rebalanceInserter)
		{
			$this->logMessage('Finally, MERGE is in progress');

			$this->rebalanceInserter->flush();

			// merge temp table with location table
			// there should be more generalized method
			Location\LocationTable::mergeRelationsFromTemporalTable(self::TREE_REBALANCE_TEMP_TABLE_NAME, false, array('LEFT_MARGIN' => 'L', 'RIGHT_MARGIN' => 'R', 'DEPTH_LEVEL' => 'D', 'ID' => 'I'));
		}
	}

	private function acceptRebalancedNode($node)
	{
		$this->createTempTable();

		if(!$this->rebalanceInserter)
		{
			if($this->dbConnType == self::DB_TYPE_ORACLE)
				$mtu = self::TREE_REBALANCE_TEMP_BLOCK_LEN_O;
			else
				$mtu = self::TREE_REBALANCE_TEMP_BLOCK_LEN;

			$this->rebalanceInserter = new BlockInserter(array(
				'tableName' => self::TREE_REBALANCE_TEMP_TABLE_NAME,
				'exactFields' => array(
					'I' => array('data_type' => 'integer'),
					'L' => array('data_type' => 'integer'),
					'R' => array('data_type' => 'integer'),
					'D' => array('data_type' => 'integer'),
				),
				'parameters' => array(
					'mtu' => $mtu
				)
			));
		}

		$this->data['processed']++;

		$this->rebalanceInserter->insert($node);
	}

	private function dropTempTable()
	{
		if($this->dbConnection->isTableExists(self::TREE_REBALANCE_TEMP_TABLE_NAME))
			$this->dbConnection->query("drop table ".self::TREE_REBALANCE_TEMP_TABLE_NAME);
	}

	private function createTempTable()
	{
		if($this->data['rebalance']['tableCreated'])
			return;

		$tableName = self::TREE_REBALANCE_TEMP_TABLE_NAME;

		if($this->dbConnection->isTableExists($tableName))
			$this->dbConnection->query("truncate table {$tableName}");
		else
		{

			if($this->dbConnType == self::DB_TYPE_ORACLE)
			{
				$this->dbConnection->query("create table {$tableName} (
					I NUMBER(18),
					L NUMBER(18),
					R NUMBER(18),
					D NUMBER(18)
				)");
			}
			else
			{
				$this->dbConnection->query("create table {$tableName} (
					I int,
					L int,
					R int,
					D int
				)");
			}

		}

		$this->data['rebalance']['tableCreated'] = true;
	}

	protected function checkFileCompletelyRead()
	{
		return $this->data['current']['bytesRead'] >= $this->data['files'][$this->data['current']['fIndex']]['size'];
	}

	protected function checkAllFilesRead()
	{
		return $this->data['current']['fIndex'] >= count($this->data['files']);
	}

	protected function checkBufferIsFull($bufferSize)
	{
		return $bufferSize >= $this->getCurrStageStepSize();
	}

	protected static function downloadFile($fileName, $storeAs, $skip404 = false, $storeTo = false)
	{
		// useless thing
		if(!$storeTo)
			$storeTo = \CTempFile::GetDirectoryName(1);

		$storeTo .= $storeAs;

		if(file_exists($storeTo))
		{
			if(!is_writable($storeTo))
				throw new Main\SystemException('Cannot remove previous '.$storeAs.' file');

			unlink($storeTo);
		}

		if(!defined('SALE_LOCATIONS_IMPORT_SOURCE_URL'))
			$query = 'http://'.self::DISTRIBUTOR_HOST.':'.self::DISTRIBUTOR_PORT.self::REMOTE_PATH.$fileName;
		else
			$query = 'http://'.SALE_LOCATIONS_IMPORT_SOURCE_URL.'/'.$fileName;

		$client = new HttpClient();

		if(!$client->download($query, $storeTo))
		{
			$eFormatted = array();
			foreach($client->getError() as $code => $desc)
				$eFormatted[] = trim($desc.' ('.$code.')');

			throw new Main\SystemException('File download failed: '.implode(', ', $eFormatted).' ('.$query.')');
		}

		$status = intval($client->getStatus());

		if($status != 200 && file_exists($storeTo))
			unlink($storeTo);

		$okay = $status == 200 || ($status == 404 && $skip404);

		// honestly we should check for all 2xx codes, but for now this is enough
		if(!$okay)
			throw new Main\SystemException('File download failed: http error '.$status.' ('.$query.')');

		return filesize($storeTo);
	}

	/**
	 * @deprecated
	 */
	protected static function cleanWorkDirectory()
	{
	}

	protected function getFileNameByIndex($i)
	{
		return self::LOCAL_SETS_PATH.sprintf(self::LOCAL_LOCATION_FILE, $i);
	}

	public function saveUserFile($inputName)
	{
		if(is_array($_FILES[$inputName]))
		{
			if($_FILES[$inputName]['error'] > 0)
				throw new Main\SystemException(self::explainFileUploadError($_FILES[$inputName]['error']));

			if(!in_array($_FILES[$inputName]['type'], array(
				'text/plain',
				'text/csv',
				'application/vnd.ms-excel',
				'application/octet-stream'
			)))
			{
				throw new Main\SystemException('Unsupported file type');
			}

			self::cleanWorkDirectory();

			list($localPath, $tmpDirCreated) = $this->getTemporalDirectory();
			$fileName = $localPath.'/'.static::USER_FILE_TEMP_NAME;

			if(!@copy($_FILES[$inputName]['tmp_name'], $fileName))
			{
				$lastError = error_get_last();
				throw new Main\SystemException($lastError['message']);
			}

			$_SESSION[static::USER_FILE_DIRECTORY_SESSION_KEY] = $localPath;
		}
		else
			throw new Main\SystemException('No file were uploaded');
	}

	protected static function explainFileUploadError($error)
	{
		switch ($error)
		{
			case UPLOAD_ERR_INI_SIZE:
				$message = 'The uploaded file exceeds the upload_max_filesize directive in php.ini';
				break;
			case UPLOAD_ERR_FORM_SIZE:
				$message = 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form';
				break;
			case UPLOAD_ERR_PARTIAL:
				$message = 'The uploaded file was only partially uploaded';
				break;
			case UPLOAD_ERR_NO_FILE:
				$message = 'No file were uploaded';
				break;
			case UPLOAD_ERR_NO_TMP_DIR:
				$message = 'Missing a temporary folder';
				break;
			case UPLOAD_ERR_CANT_WRITE:
				$message = 'Failed to write file to disk';
				break;
			case UPLOAD_ERR_EXTENSION:
				$message = 'File upload stopped by extension';
				break;

			default:
				$message = 'Unknown upload error';
				break;
		}
		return $message;
	}

	// all this mess is only to get import work on Bitrix24 (which does not provide any temporal directory in a typical meaning)
	protected function getTemporalDirectory()
	{
		$wasCreated = false;
		if((string) $this->data['LOCAL_PATH'] != '' && \Bitrix\Main\IO\Directory::isDirectoryExists($this->data['LOCAL_PATH']))
		{
			$localPath = $this->data['LOCAL_PATH'];
		}
		else
		{
			$wasCreated = true;
			$localPath = \CTempFile::GetDirectoryName(10);
			if(!\Bitrix\Main\IO\Directory::isDirectoryExists($localPath))
				\Bitrix\Main\IO\Directory::createDirectory($localPath);
		}

		return array($localPath, $wasCreated);
	}

	protected static function createDirectory($path)
	{
		if(!\Bitrix\Main\IO\Directory::isDirectoryExists($path))
		{
			\Bitrix\Main\IO\Directory::createDirectory($path);
		}
	}

	protected static function deleteDirectory($path)
	{
		if(\Bitrix\Main\IO\Directory::isDirectoryExists($path))
		{
			\Bitrix\Main\IO\Directory::deleteDirectory($path);
		}
	}

	protected function normalizeQueryArray($value)
	{
		$result = array();
		if(is_array($value))
		{
			foreach($value as $v)
			{
				if(strlen($v))
					$result[] = $this->parseQueryCode($v);
			}
		}

		$result = array_unique($result);
		sort($result, SORT_STRING);

		return $result;
	}

	protected static function parseQueryCode($value)
	{
		$value = ToLower(trim($value));

		if(!preg_match('#^[a-z0-9]+$#i', $value))
			throw new Main\SystemException('Bad request parameter');

		return $value;
	}

	public function turnOffCache()
	{
		$this->useCache = false;
	}

	########################################################
	## static part is used in places like wizards, etc

	public static function getExistedTypes()
	{
		$existed = array();
		$res = Location\TypeTable::getList(array('select' => array('ID', 'CODE', 'SORT')));
		while($item = $res->fetch())
			$existed[$item['CODE']] = $item['ID'];

		return $existed;
	}

	public static function getExistedServices()
	{
		$existed = array();
		$res = Location\ExternalServiceTable::getList(array('select' => array('ID', 'CODE')));
		while($item = $res->fetch())
			$existed[$item['CODE']] = $item['ID'];

		return $existed;
	}

	public static function createTypes($types, $existed = false)
	{
		// read existed
		if($existed === false)
			$existed = static::getExistedTypes();

		// here we try to add type names for ALL languages
		$langs = Location\Admin\NameHelper::getLanguageList();

		foreach($types as $line)
		{
			// for sure
			unset($line['SELECTORLEVEL']);
			unset($line['DEFAULTSELECT']);

			$names = array();

			if(!is_array($line['NAME']))
				$line['NAME'] = array();

			if(is_array($langs))
			{
				foreach($langs as $lid => $f)
				{
					$names[ToUpper($lid)] = static::getTranslatedName($line['NAME'], $lid);
				}
				$line['NAME'] = $names;
			}

			if(!isset($existed[$line['CODE']]))
			{
				$existed[$line['CODE']] = static::createType($line);
			}
			else
			{
				// ensure it has all appropriate translations
				// we can not use ::updateMultipleForOwner() here, because user may rename his types manually
				Location\Name\TypeTable::addAbsentForOwner($existed[$line['CODE']], $names);
			}
		}

		return $existed;
	}

	protected static function getTranslatedName($names, $languageId)
	{
		$languageIdMapped = 	ToUpper(Location\Admin\NameHelper::mapLanguage($languageId));
		$languageId = 			ToUpper($languageId);

		if(is_array($names[$languageId]) && (string) $names[$languageId]['NAME'] != '')
			return $names[$languageId];

		if(is_array($names[$languageIdMapped]) && (string) $names[$languageIdMapped]['NAME'] != '')
			return $names[$languageIdMapped];

		return $names['EN'];
	}

	public static function createType($type)
	{
		$map = Location\TypeTable::getMap($type);

		if(is_array($type))
		{
			foreach($type as $fld => $val)
			{
				if(!isset($map[$fld]))
				{
					unset($type[$fld]);
				}
			}
		}

		$res = Location\TypeTable::add($type);
		if(!$res->isSuccess())
			throw new Main\SystemException('Type creation failed: '.implode(', ', $res->getErrorMessages()));

		return $res->getId();
	}

	public static function createService($service)
	{
		$res = Location\ExternalServiceTable::add($service);
		if(!$res->isSuccess())
			throw new Main\SystemException('External service creation failed: '.implode(', ', $res->getErrorMessages()));

		return $res->getId();
	}

	public static function getTypeMap($file)
	{
		$csvReader = new CSVReader();
		$csvReader->LoadFile($file);

		$types = array();
		$i = 0;
		while($type = $csvReader->FetchAssoc())
		{
			if($i) // fix for CSVReader parent class bug
			{
				unset($type['SELECTORLEVEL']);
				unset($type['DEFAULTSELECT']);

				$types[$type['CODE']] = $type;
			}

			$i++;
		}

		return $types;
	}

	public static function getServiceMap($file)
	{
		$csvReader = new CSVReader();
		$csvReader->LoadFile($file);

		$services = array();
		while($service = $csvReader->FetchAssoc())
			$services[$service['CODE']] = $service;

		return $services;
	}

	// this is generally for e-shop installer
	public static function importFile(&$descriptior)
	{
		$timeLimit = ini_get('max_execution_time');
		if ($timeLimit < $descriptior['TIME_LIMIT']) set_time_limit($descriptior['TIME_LIMIT'] + 5);

		$endTime = time() + $descriptior['TIME_LIMIT'];

		if($descriptior['STEP'] == 'rebalance')
		{
			Location\LocationTable::resort();
			Location\LocationTable::resetLegacyPath();
			$descriptior['STEP'] = 'done';
		}

		if($descriptior['STEP'] == 'import')
		{
			if(!isset($descriptior['DO_SYNC']))
			{
				$res = \Bitrix\Sale\Location\LocationTable::getList(array('select' => array('CNT')))->fetch();
				$descriptior['DO_SYNC'] = intval($res['CNT'] > 0);
			}

			if(!isset($descriptior['TYPES']))
			{
				$descriptior['TYPE_MAP'] = static::getTypeMap($descriptior['TYPE_FILE']);
				$descriptior['TYPES'] = static::createTypes($descriptior['TYPE_MAP']);

				$descriptior['SERVICE_MAP'] = static::getServiceMap($descriptior['SERVICE_FILE']);
				$descriptior['SERVICES'] = static::getExistedServices();
			}

			$csvReader = new CSVReader();
			$csvReader->LoadFile($descriptior['FILE']);

			while(time() < $endTime)
			{
				$block = $csvReader->ReadBlockLowLevel($descriptior['POS']/*changed inside*/, 10);

				if(!count($block))
					break;

				foreach($block as $item)
				{
					if($descriptior['DO_SYNC'])
					{
						$id = static::checkLocationCodeExists($item['CODE']);
						if($id)
						{
							$descriptior['CODES'][$item['CODE']] = $id;
							continue;
						}
					}

					// type
					$item['TYPE_ID'] = $descriptior['TYPES'][$item['TYPE_CODE']];
					unset($item['TYPE_CODE']);

					// parent id
					if(strlen($item['PARENT_CODE']))
					{
						if(!isset($descriptior['CODES'][$item['PARENT_CODE']]))
							$descriptior['CODES'][$item['PARENT_CODE']] = static::checkLocationCodeExists($item['PARENT_CODE']);

						$item['PARENT_ID'] = $descriptior['CODES'][$item['PARENT_CODE']];
					}
					unset($item['PARENT_CODE']);

					// ext
					if(is_array($item['EXT']))
					{
						foreach($item['EXT'] as $code => $values)
						{
							if(!empty($values))
							{
								if(!isset($descriptior['SERVICES'][$code]))
								{
									$descriptior['SERVICES'][$code] = static::createService(array(
										'CODE' => $code
									));
								}

								if($code == 'ZIP_LOWER')
								{
									if(strlen($values[0]) <= 0)
										continue;

									$values = explode(',', $values[0]);

									if(!is_array($values))
										continue;

									$values = array_unique($values);
								}

								if(is_array($values))
								{
									foreach($values as $value)
									{
										if(strlen($value) <= 0)
											continue;

										$item['EXTERNAL'][] = array(
											'SERVICE_ID' => $descriptior['SERVICES'][$code],
											'XML_ID' => $value
										);
									}
								}
							}
						}
					}
					unset($item['EXT'], $item['ZIP_LOWER']);

					$res = Location\LocationTable::addExtended(
						$item,
						array(
							'RESET_LEGACY' => false,
							'REBALANCE' => false
						)
					);

					if(!$res->isSuccess())
						throw new Main\SystemException('Cannot create location');

					$descriptior['CODES'][$item['CODE']] = $res->getId();
				}
			}

			if(!count($block))
			{
				unset($descriptior['CODES']);
				$descriptior['STEP'] = 'rebalance';
			}
		}

		return $descriptior['STEP'] == 'done';
	}

	public function provideEnFromRu(&$data)
	{
		// restore at least "EN" translation
		if(is_array($data))
		{
			if(is_array($data['NAME']) && is_array($data['NAME']['RU']))
			{
				if(!is_array($data['NAME']['EN']))
					$data['NAME']['EN'] = array();

				foreach($data['NAME']['RU'] as $k => $v)
				{
					if((string) $data['NAME']['EN'][$k] == '')
						$data['NAME']['EN'][$k] = Location\Admin\NameHelper::translitFromUTF8($data['NAME']['RU'][$k]);
				}
			}
		}
	}
}