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

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

/**
* DBList
*
* Desciption
* @package kernel4
*/
class kDBList extends kDBBase {

	/**
	 * Holds totals for fields specified in config
	 *
	 * @var Array
	 */
	var $Totals = Array ();

	/**
	* Description
	*
	* @var array
	* @access public
	*/
	var $OrderFields;

	/**
	* Holds counted total number of records in the query - without pagination (system+user filters)
	*
	* @var int
	* @access public
	*/
	var $RecordsCount;


	/**
	 * Records count with system filters only applied
	 *
	 * @var int
	 * @access private
	 */
	var $NoFilterCount = 0;

	/**
	 * Record count selected to be
	 * showed on current page
	 *
	 * @var int
	 */
	var $SelectedCount = 0;

	/**
	 * Array of records selected
	 *
	 * @var Array
	 * @access private
	 */
	var $Records;

	var $CurrentIndex = 0;

	/**
	* List items per-page
	*
	* @var int
	* @access public
	*/
	var $PerPage;

	/**
	* Pages count in list based on PerPage & RecordsCount attributes
	*
	* @var int
	* @access public
	*/
	var $TotalPages;

	/**
	* Description
	*
	* @var int
	* @access public
	*/

	var $Direction;
	/**
	* Holds current page number - used when forming LIMIT clause of SELECT statement
	*
	* @var int
	* @access public
	*/
	var $Page;

	/**
	* Holds offset for LIMIT clause, calculated in {@link kDBList::PerPage()}
	*
	* @var int
	* @access private
	*/
	var $Offset;

	/**
	 * Count SQL was already issued on query
	 *
	 * @var bool
	 * @access private
	 */
	var $hasCounted = false;

	/**
	 * Holds list WHERE filter object
	 *
	 * @var kMultipleFilter
	 * @access private
	 */
	var $WhereFilter = Array (FLT_SYSTEM => null, FLT_NORMAL => null, FLT_SEARCH => null, FLT_VIEW => null);

	/**
	 * Holds list HAVING filter object
	 *
	 * @var kMultipleFilter
	 * @access private
	 */
	var $HavingFilter = Array (FLT_SYSTEM => null, FLT_NORMAL => null, FLT_SEARCH => null, FLT_VIEW => null);

	var $AggregateFilter = Array (FLT_SYSTEM => null, FLT_NORMAL => null);

	var $GroupByFields = Array ();

	var $Queried = false;

	var $Counted = false;

	var $TotalsCalculated = false;

	/**
	 * Name of the grid, used to display the list
	 *
	 * @var string
	 */
	var $gridName = '';

	/**
	 * Identifies this list as main on the page, that allows to react on "page", "per_page" and "sort_by" parameters from url
	 *
	 * @var bool
	 */
	var $mainList = false;

	/**
	* Creates kDBList
	*
	* @return kDBList
	*/
	function kDBList()
	{
		parent::kDBBase();
		$this->OrderFields = Array();


		$filters = $this->getFilterStructure();

		foreach ($filters as $filter_params) {
			$filter =& $this->$filter_params['type'];
			$filter[ $filter_params['class'] ] =& $this->Application->makeClass('kMultipleFilter', $filter_params['join_using']);
		}

		$this->PerPage = -1;
	}

	function setGridName($grid_name)
	{
		$this->gridName = $grid_name;
	}

	/**
	 * Returns information about all possible filter types
	 *
	 * @return Array
	 */
	function getFilterStructure()
	{
		$filters = 	Array (
							Array ('type' => 'WhereFilter', 'class' => FLT_SYSTEM, 'join_using' => FLT_TYPE_AND),
							Array ('type' => 'WhereFilter', 'class' => FLT_NORMAL, 'join_using' => FLT_TYPE_OR),
							Array ('type' => 'WhereFilter', 'class' => FLT_SEARCH, 'join_using' => FLT_TYPE_OR),
							Array ('type' => 'WhereFilter', 'class' => FLT_VIEW, 'join_using' => FLT_TYPE_AND),
							Array ('type' => 'WhereFilter', 'class' => FLT_CUSTOM, 'join_using' => FLT_TYPE_AND),

							Array ('type' => 'HavingFilter', 'class' => FLT_SYSTEM, 'join_using' => FLT_TYPE_AND),
							Array ('type' => 'HavingFilter', 'class' => FLT_NORMAL, 'join_using' => FLT_TYPE_OR),
							Array ('type' => 'HavingFilter', 'class' => FLT_SEARCH, 'join_using' => FLT_TYPE_OR),
							Array ('type' => 'HavingFilter', 'class' => FLT_VIEW, 'join_using' => FLT_TYPE_AND),
							Array ('type' => 'HavingFilter', 'class' => FLT_CUSTOM, 'join_using' => FLT_TYPE_AND),

							Array ('type' => 'AggregateFilter', 'class' => FLT_SYSTEM, 'join_using' => FLT_TYPE_AND),
							Array ('type' => 'AggregateFilter', 'class' => FLT_NORMAL, 'join_using' => FLT_TYPE_OR),
							Array ('type' => 'AggregateFilter', 'class' => FLT_VIEW, 'join_using' => FLT_TYPE_AND),
					);

		return $filters;
	}

	/**
	 * Adds new or replaces old filter with same name
	 *
	 * @param string $name filter name (for internal use)
	 * @param string $clause where/having clause part (no OR/AND allowed)
	 * @param int $filter_type is filter having filter or where filter
	 * @param int $filter_scope filter subtype: FLT_NORMAL,FLT_SYSTEM,FLT_SEARCH,FLT_VIEW,FLT_CUSTOM
	 * @access public
	 */
	function addFilter($name, $clause, $filter_type = WHERE_FILTER, $filter_scope = FLT_SYSTEM)
	{
		$filter_source = Array(	WHERE_FILTER		=>	'WhereFilter',
								HAVING_FILTER		=>	'HavingFilter',
								AGGREGATE_FILTER	=>	'AggregateFilter');
		$filter_name = $filter_source[$filter_type];

		$filter =& $this->$filter_name;
		$filter =& $filter[$filter_scope];
		$filter->addFilter($name,$clause);
	}

	/**
	 * Reads filter content
	 *
	 * @param string $name filter name (for internal use)
	 * @param int $filter_type is filter having filter or where filter
	 * @param int $filter_scope filter subtype: FLT_NORMAL,FLT_SYSTEM,FLT_SEARCH,FLT_VIEW,FLT_CUSTOM
	 * @access public
	 */
	function getFilter($name, $filter_type = WHERE_FILTER, $filter_scope = FLT_SYSTEM)
	{
		$filter_source = Array(	WHERE_FILTER		=>	'WhereFilter',
								HAVING_FILTER		=>	'HavingFilter',
								AGGREGATE_FILTER	=>	'AggregateFilter');
		$filter_name = $filter_source[$filter_type];

		$filter =& $this->$filter_name;
		$filter =& $filter[$filter_scope];
		return $filter->getFilter($name);
	}

	/**
	 * Removes specified filter from filters list
	 *
	 * @param string $name filter name (for internal use)
	 * @param int $filter_type is filter having filter or where filter
	 * @param int $filter_scope filter subtype: FLT_NORMAL,FLT_SYSTEM,FLT_SEARCH,FLT_VIEW,FLT_CUSTOM
	 * @access public
	 */
	function removeFilter($name, $filter_type = WHERE_FILTER, $filter_scope = FLT_SYSTEM)
	{
		$filter_source = Array(	WHERE_FILTER		=>	'WhereFilter',
								HAVING_FILTER		=>	'HavingFilter',
								AGGREGATE_FILTER	=>	'AggregateFilter');
		$filter_name = $filter_source[$filter_type];

		$filter =& $this->$filter_name;
		$filter =& $filter[$filter_scope];
		$filter->removeFilter($name);
	}

	/**
	 * Clear list filters
	 *
	 */
	function clearFilters()
	{
		$filters = $this->getFilterStructure();

		foreach ($filters as $filter_params) {
			$filter =& $this->$filter_params['type'];
			$filter[ $filter_params['class'] ]->clearFilters();
		}
	}

	/**
	* Counts the total number of records base on the query resulted from {@link kDBList::GetSelectSQL()}
	*
	* The method modifies the query to substitude SELECT part (fields listing) with COUNT(*).
	* Special care should be applied when working with lists based on grouped queries, all aggregate function fields
	* like SUM(), AVERAGE() etc. should be added to CountedSQL by using {@link kDBList::SetCountedSQL()}
	*
	* @access public
	* @param string
	* @return void
	*/
	function CountRecs()
	{
		$all_sql = $this->GetSelectSQL(true,false);
		$sql = $this->getCountSQL($all_sql);

		$this->Counted = true;

		if( $this->GetGroupClause() )
		{
    		$this->RecordsCount = count( $this->Conn->GetCol($sql) );
		}
		else
		{
			$this->RecordsCount = (int)$this->Conn->GetOne($sql);
		}

	    $system_sql = $this->GetSelectSQL(true,true);
	    if($system_sql == $all_sql) //no need to query the same again
	    {
	    	$this->NoFilterCount = $this->RecordsCount;
	    	return;
	    }

		$sql = $this->getCountSQL($system_sql);
		if( $this->GetGroupClause() )
		{
			$this->NoFilterCount = count( $this->Conn->GetCol($sql) );
		}
		else
		{
			$this->NoFilterCount = (int)$this->Conn->GetOne($sql);
		}
	}

	function GetNoFilterCount()
	{
		if (!$this->Counted) {
			$this->CountRecs();
		}
		return $this->NoFilterCount;
	}

	function GetRecordsCount()
	{
		if (!$this->Counted) {
			$this->CountRecs();
		}
		return $this->RecordsCount;
	}

	function getCountSQL($sql)
	{
		if ( preg_match("/^\s*SELECT\s+DISTINCT(.*?)FROM(?!_)/is",$sql,$regs ) )
		{
			return preg_replace("/^\s*SELECT\s+DISTINCT(.*?)FROM(?!_)/is", "SELECT COUNT(DISTINCT ".$regs[1].") AS count FROM", $sql);
		}
		else
		{
			return preg_replace("/^\s*SELECT(.*?)FROM(?!_)/is", "SELECT COUNT(*) AS count FROM ", $sql);
		}
	}

	/**
	* Queries the database with SQL resulted from {@link kDBList::GetSelectSQL()} and stores result in {@link kDBList::SelectRS}
	*
	* All the sorting, pagination, filtration of the list should be set prior to calling Query().
	*
	* @access public
	* @param string
	* @return void
	*/
	function Query($force=false)
	{
		if (!$force && $this->Queried) return true;
		$q = $this->GetSelectSQL();

		//$rs = $this->Conn->SelectLimit($q, $this->PerPage, $this->Offset);

		//in case we have not counted records try to select one more item to find out if we have something more than perpage
		$limit = $this->Counted ? $this->PerPage : $this->PerPage+1;

		$sql = $q.' '.$this->Conn->getLimitClause($this->Offset,$limit);

		$this->Records = $this->Conn->Query($sql);

		if (!$this->Records && ($this->Page > 1)) {
			// no records & page > 1, try to reset to 1st page (works only when list in not counted before)
			$this->Application->StoreVar($this->getPrefixSpecial() . '_Page', 1, true);
			$this->SetPage(1);
			$this->Query($force);
		}

		$this->SelectedCount = count($this->Records);
		if (!$this->Counted) $this->RecordsCount = $this->SelectedCount;
		if (!$this->Counted && $this->SelectedCount > $this->PerPage && $this->PerPage != -1) $this->SelectedCount--;

		if ($this->Records === false) {
			//handle errors here
			return false;
		}
		$this->Queried = true;

		$query_event = new kEvent($this->getPrefixSpecial() . ':OnAfterListQuery');
		$this->Application->HandleEvent($query_event);

		return true;
	}

	/**
	 * Adds one more record to list virtually and updates all counters
	 *
	 * @param Array $record
	 */
	function addRecord($record)
	{
		$this->Records[] = $record;
		$this->SelectedCount++;
		$this->RecordsCount++;
	}

	function CalculateTotals()
	{
		$fields = Array();
		$this->Totals = Array();

		if ($this->gridName) {
			$grids = $this->Application->getUnitOption($this->Prefix, 'Grids');
			$grid_fields = $grids[$this->gridName]['Fields'];
		}
		else {
			$grid_fields = $this->Fields;
		}

		foreach ($grid_fields as $field_name => $field_options) {
			if ($this->gridName && array_key_exists('totals', $field_options) && $field_options['totals']) {
				$totals = $field_options['totals'];
			}
			elseif (array_key_exists('totals', $this->Fields[$field_name]) && $this->Fields[$field_name]['totals']) {
				$totals = $this->Fields[$field_name]['totals'];
			}
			else {
				continue;
			}

			$calculated_field = array_key_exists($field_name, $this->CalculatedFields) && array_key_exists($field_name, $this->VirtualFields);
			$db_field = !array_key_exists($field_name, $this->VirtualFields);

			if ($calculated_field || $db_field) {
				$field_expression = $calculated_field ? $this->CalculatedFields[$field_name] : '`'.$this->TableName.'`.`'.$field_name.'`';
				$fields[$field_name] = $totals.'('.$field_expression.') AS '.$field_name.'_'.$totals;
			}
		}

		if(!$fields) return false;

		$sql = $this->GetSelectSQL(true, false);
		$fields = str_replace('%1$s', $this->TableName, implode(', ', $fields));

		if ( preg_match("/DISTINCT(.*?)FROM(?!_)/is",$sql,$regs ) )
		{
			$sql = preg_replace("/^\s*SELECT DISTINCT(.*?)FROM(?!_)/is", 'SELECT '.$fields.' FROM', $sql);
		}
		else
		{
			$sql = preg_replace("/^\s*SELECT(.*?)FROM(?!_)/is", 'SELECT '.$fields.' FROM ', $sql);
		}

		$totals = $this->Conn->Query($sql);

		foreach($totals as $totals_row)
		{
			foreach($totals_row as $total_field => $field_value)
			{
				if(!isset($this->Totals[$total_field])) $this->Totals[$total_field] = 0;
				$this->Totals[$total_field] += $field_value;
			}
		}
		$this->TotalsCalculated = true;
	}

	function getTotal($field, $total_function)
	{
		if (!$this->TotalsCalculated) {
			$this->CalculateTotals();
		}

		return $this->Totals[$field . '_' . $total_function];
	}

	function setTotal($field, $total_function, $value)
	{
		$this->Totals[$field . '_' . $total_function] = $value;
	}

	function getTotalFunction($field)
	{
		if ($this->gridName) {
			$grids = $this->Application->getUnitOption($this->Prefix, 'Grids');
			$field_options = $grids[$this->gridName]['Fields'][$field];
		}
		else {
			$field_options = $this->Fields[$field];
		}

		if ($this->gridName && array_key_exists('totals', $field_options) && $field_options['totals']) {
			return $field_options['totals'];
		}
		elseif (array_key_exists('totals', $this->Fields[$field]) && $this->Fields[$field]['totals']) {
			return $this->Fields[$field]['totals'];
		}

		return false;
	}

	function GetFormattedTotal($field, $total_function)
	{
		$val = $this->getTotal($field, $total_function);
		$options = $this->GetFieldOptions($field);
		$res = $val;
		if (isset($options['formatter'])) {
			$formatter =& $this->Application->recallObject($options['formatter']);
			$res = $formatter->Format($val, $field, $this );
		}
		return $res;
	}

	/**
	* Builds full select query except for LIMIT clause
	*
	* @access public
	* @return string
	*/
	function GetSelectSQL($for_counting=false,$system_filters_only=false)
	{
		$q = parent::GetSelectSQL($this->SelectClause);
		$q = !$for_counting ? $this->addCalculatedFields($q, 0) : str_replace('%2$s', '', $q);

		$where = $this->GetWhereClause($for_counting,$system_filters_only);
		$having = $this->GetHavingClause($for_counting,$system_filters_only);
		$order = $this->GetOrderClause();
		$group = $this->GetGroupClause();

		if (!empty($where)) $q .= ' WHERE ' . $where;
		if (!empty($group)) $q .= ' GROUP BY ' . $group;
		if (!empty($having)) $q .= ' HAVING ' . $having;
		if ( !$for_counting && !empty($order) ) $q .= ' ORDER BY ' . $order;

		return $this->replaceModePrefix( str_replace('%1$s', $this->TableName, $q) );
	}

	/**
	 * Enter description here...
	 *
	 * @param string $clause where clause to extract calculated fields from
	 * @param int $aggregated 0 - having + aggregated, 1 - having only, 2 - aggregated only
	 * @return string
	 */
	function extractCalculatedFields($clause, $aggregated = 1)
	{
		$fields = $this->getCalculatedFields($aggregated);
		if (is_array($fields) && count($fields) > 0) {
			foreach ($fields as $field_name => $field_expression) {
				$clause = preg_replace('/(\\(+)[(,` ]*'.$field_name.'[` ]{1}/', '\1 ('.$field_expression.') ', $clause);
				$clause = preg_replace('/[,` ]{1}'.$field_name.'[` ]{1}/', ' ('.$field_expression.') ', $clause);
			}
		}
		return $clause;
	}

	/**
	* Returns WHERE clause of the query
	*
	* @access public
	* @param bool $for_counting merge where filters with having filters + replace field names for having fields with their values
	* @return string
	*/
	function GetWhereClause($for_counting=false,$system_filters_only=false)
	{
		$where =& $this->Application->makeClass('kMultipleFilter');

		$where->addFilter('system_where', $this->WhereFilter[FLT_SYSTEM] );

		if (!$system_filters_only) {
			$where->addFilter('view_where', $this->WhereFilter[FLT_VIEW] );
			$search_w = $this->WhereFilter[FLT_SEARCH]->getSQL();
			if ($search_w || $for_counting) { // move search_having to search_where in case search_where isset or we are counting
				$search_h = $this->extractCalculatedFields( $this->HavingFilter[FLT_SEARCH]->getSQL() );
				$search_w = ($search_w && $search_h) ? $search_w.' OR '.$search_h : $search_w.$search_h;
				$where->addFilter('search_where', $search_w );
			}

			// CUSTOM
			$search_w = $this->WhereFilter[FLT_CUSTOM]->getSQL();
			if ($search_w || $for_counting) { // move search_having to search_where in case search_where isset or we are counting
				$search_h = $this->extractCalculatedFields( $this->HavingFilter[FLT_CUSTOM]->getSQL() );
				$search_w = ($search_w && $search_h) ? $search_w.' AND '.$search_h : $search_w.$search_h;
				$where->addFilter('custom_where', $search_w );
			}
			// CUSTOM
		}

		if( $for_counting ) // add system_having and view_having to where
		{
			$where->addFilter('system_having', $this->extractCalculatedFields($this->HavingFilter[FLT_SYSTEM]->getSQL()) );
			if (!$system_filters_only) $where->addFilter('view_having', $this->extractCalculatedFields( $this->HavingFilter[FLT_VIEW]->getSQL() ) );
		}

		return $where->getSQL();
	}

	/**
	 * Depricated method
	 *
	 * @param string $clause
	 * @todo REMOVE
	 */
	function SetWhereClause($clause)
	{
		if ($this->Application->isDebugMode()) {
			global $debugger;
			$debugger->appendTrace();
		}

		trigger_error('Depricated method <b>kDBList->SetWhereClause</b>. Use <b>kDBList->addFilter</b> instead.', defined('E_USER_DEPRECATED') ? E_USER_DEPRECATED : E_USER_NOTICE);
	}

	/**
	* Returns HAVING clause of the query
	*
	* @param bool $for_counting don't return having filter in case if this is counting sql
	* @param bool $system_filters_only return only system having filters
	* @param int $aggregated 0 - aggregated and having, 1 - having only, 2 - aggregated only
	* @return string
	* @access public
	*/
	function GetHavingClause($for_counting=false, $system_filters_only=false, $aggregated = 0)
	{
		if ($for_counting) {
			$aggregate_filter =& $this->Application->makeClass('kMultipleFilter');
			$aggregate_filter->addFilter('aggregate_system', $this->AggregateFilter[FLT_SYSTEM]);
			if (!$system_filters_only) {
				$aggregate_filter->addFilter('aggregate_view', $this->AggregateFilter[FLT_VIEW]);
			}
			return $this->extractCalculatedFields($aggregate_filter->getSQL(), 2);
		}

		$having =& $this->Application->makeClass('kMultipleFilter');

		$having->addFilter('system_having', $this->HavingFilter[FLT_SYSTEM] );
		if ($aggregated == 0) {
			if (!$system_filters_only) {
                $having->addFilter('view_aggregated', $this->AggregateFilter[FLT_VIEW] );
            }
            $having->addFilter('system_aggregated', $this->AggregateFilter[FLT_SYSTEM]);
		}

		if (!$system_filters_only) {
			$having->addFilter('view_having', $this->HavingFilter[FLT_VIEW] );
			$having->addFilter('custom_having', $this->HavingFilter[FLT_CUSTOM] );
			$search_w = $this->WhereFilter[FLT_SEARCH]->getSQL();
			if (!$search_w) {
				$having->addFilter('search_having', $this->HavingFilter[FLT_SEARCH] );
			}
		}

		return $having->getSQL();
	}

	/**
	* Returns GROUP BY clause of the query
	*
	* @access public
	* @return string
	*/
	function GetGroupClause()
	{
		return $this->GroupByFields ? implode(',', $this->GroupByFields) : '';
	}

	function AddGroupByField($field)
	{
		$this->GroupByFields[$field] = $field;
	}

	function RemoveGroupByField($field)
	{
		unset($this->GroupByFields[$field]);
	}

	/**
	* Adds order field to ORDER BY clause
	*
	* @access public
	* @param string $field Field name
	* @param string $direction Direction of ordering (asc|desc)
	* @param bool $is_expression this is expression, that should not be escapted by "`" symbols
	* @return void
	*/
	function AddOrderField($field, $direction = 'asc', $is_expression = false)
	{
		// original multilanguage field - convert to current lang field
		$formatter = isset($this->Fields[$field]['formatter']) ? $this->Fields[$field]['formatter'] : false;

		if ($formatter == 'kMultiLanguage' && !isset($this->Fields[$field]['master_field'])) {
			// for now kMultiLanguage formatter is only supported for real (non-virtual) fields
			$is_expression = true;
			$field = $this->getMLSortField($field);
		}

		if (!isset($this->Fields[$field]) && $field != 'RAND()' && !$is_expression) {
			trigger_error('<span class="debug_error">Incorrect sorting</span> defined (field = <b>'.$field.'</b>; direction = <b>'.$direction.'</b>) in config for prefix <b>'.$this->Prefix.'</b>', E_USER_NOTICE);
		}

		$this->OrderFields[] = Array($field, $direction, $is_expression);
	}

	/**
	 * Returns expression, used to sort given multilingual field
	 *
	 * @param string $field
	 * @return string
	 */
	function getMLSortField($field)
	{
		$table_name = '`' . $this->TableName . '`';
		$lang = $this->Application->GetVar('m_lang');
		$primary_lang = $this->Application->GetDefaultLanguageId();

		$ret = 'IF(COALESCE(%1$s.l' . $lang . '_' . $field . ', ""), %1$s.l' . $lang . '_' . $field . ', %1$s.l' . $primary_lang . '_' . $field . ')';

		return sprintf($ret, $table_name);
	}

	/**
	* Removes all order fields
	*
	* @access public
	* @return void
	*/
	function ClearOrderFields()
	{
		$this->OrderFields = Array();
	}

	/**
	* Returns ORDER BY Clause of the query
	*
	*	The method builds order by clause by iterating {@link kDBList::OrderFields} array and concatenating it.
	*
	* @access public
	* @return string
	*/
	function GetOrderClause()
	{
		$ret = '';
		foreach ($this->OrderFields as $field) {

			$name = $field[0];
			$ret .= isset($this->Fields[$name]) && !isset($this->VirtualFields[$name]) ? '`'.$this->TableName.'`.' : '';

			if ($field[0] == 'RAND()' || $field[2]) {
				$ret .= $field[0].' '.$field[1].',';
			}
			else {
				$ret .= (strpos($field[0], '.') === false ? '`'.$field[0] . '`' : $field[0]) . ' ' . $field[1] . ',';
			}
		}
		$ret = rtrim($ret, ',');
		return $ret;
	}

	function GetOrderField($pos = NULL, $no_default = false)
	{
		if ( !(isset($this->OrderFields[$pos]) && $this->OrderFields[$pos]) && !$no_default ) {
			$pos = 0;
		}

		if ( isset($this->OrderFields[$pos][0]) ) {
			$field = $this->OrderFields[$pos][0];
			$lang = $this->Application->GetVar('m_lang');

			if ( preg_match('/^IF\(COALESCE\(.*?\.(l' . $lang . '_.*?), ""\),/', $field, $regs) ) {
				// undo result of kDBList::getMLSortField method
				return $regs[1];
			}

			return $field;
		}

		return '';
	}

	function GetOrderDirection($pos = NULL, $no_default = false)
	{
		if ( !(isset($this->OrderFields[$pos]) && $this->OrderFields[$pos]) && !$no_default ) {
			$pos = 0;
		}

		return isset($this->OrderFields[$pos][1]) ? $this->OrderFields[$pos][1] : '';
	}

	/**
	 * Return unformatted field value
	 *
	 * @param string $name
	 * @return string
	 * @access public
	 */
	function GetDBField($name)
	{
		$row =& $this->getCurrentRecord();

		if (defined('DEBUG_MODE') && DEBUG_MODE && $this->Queried && !array_key_exists($name, $row)) {
			if ($this->Application->isDebugMode()) {
				$this->Application->Debugger->appendTrace();
			}

			trigger_error('Field "<strong>' . $name . '</strong>" doesn\'t exist in prefix <strong>' . $this->getPrefixSpecial() . '</strong>', E_USER_WARNING);
			return 'NO SUCH FIELD';
		}

		// return "null" for missing fields, because formatter require such behaviour !
		return array_key_exists($name, $row) ? $row[$name] : null;
	}

	/**
	 * Returns ID of currently processed record
	 *
	 * @return int
	 * @access public
	 */
	function GetID()
	{
		return $this->Queried ? $this->GetDBField($this->IDField) : null;
	}

	/**
	 * Allows kDBTagProcessor.SectionTitle to detect if it's editing or new item creation
	 *
	 * @return bool
	 */
	function IsNewItem()
	{
		// no such thing as NewItem for lists :)
		return false;
	}

	function HasField($name)
	{
		$row =& $this->getCurrentRecord();
		return isset($row[$name]);
	}

	function GetFieldValues()
	{
		return $this->getCurrentRecord();
	}

	/**
	 * Returns current record from list
	 *
	 * @param int $offset Offset relative to current record index
	 * @return Array
	 */
	function &getCurrentRecord($offset = 0)
	{
		$record_index = $this->CurrentIndex + $offset;

		if ($record_index >=0 && $record_index < $this->SelectedCount) {
			return $this->Records[$record_index];
		}

		return false;
	}

	/**
	 * Goes to record with given index
	 *
	 * @param int $index
	 */
	function GoIndex($index)
	{
		$this->CurrentIndex = $index;
	}

	/**
	* Description
	*
	* @access public
	* @param string
	* @return void
	*/
	function GoFirst()
	{
		$this->CurrentIndex = 0;
	}

	/**
	* Description
	*
	* @access public
	* @return void
	*/
	function GoNext()
	{
		$this->CurrentIndex++;
	}

	/**
	* Description
	*
	* @access public
	* @return void
	*/
	function GoPrev()
	{
		if ($this->CurrentIndex>0)
			$this->CurrentIndex--;
	}

	/**
	* Description
	*
	* @access public
	* @return bool
	*/
	function EOL()
	{
		return ($this->CurrentIndex >= $this->SelectedCount);
	}

	/**
	* Description
	*
	* @access public
	* @param string
	* @return void
	*/
	function GetTotalPages()
	{
		if (!$this->Counted) $this->CountRecs();
		if ($this->PerPage == -1) return 1;
		$this->TotalPages = (($this->RecordsCount - ($this->RecordsCount % $this->PerPage)) / $this->PerPage) // integer part of division
								+ (($this->RecordsCount % $this->PerPage) != 0);  // adds 1 if there is a reminder
		return $this->TotalPages;
	}

	/**
	* Sets number of records to query per page
	*
	* @access public
	* @param int $per_page Number of records to display per page
	* @return void
	*/
	function SetPerPage($per_page)
	{
		$this->PerPage = $per_page;
	}

	function GetPerPage()
	{
		return $this->PerPage == -1 ? $this->RecordsCount : $this->PerPage;
	}

	/**
	* Description
	*
	* @access public
	* @param int $page
	* @return void
	*/
	function SetPage($page)
	{
		if ($this->PerPage == -1) {
			$this->Page = 1;
			return;
		}
		if ($page < 1) $page = 1;
		$this->Offset = ($page-1)*$this->PerPage;
		if ($this->Counted && $this->Offset > $this->RecordsCount) {
			$this->SetPage(1);
		}
		else {
			$this->Page = $page;
		}
		//$this->GoFirst();
	}

	/**
	* Sets current item field value
	* (doesn't apply formatting)
	*
	* @access public
	* @param string $name Name of the field
	* @param mixed $value Value to set the field to
	* @return void
	*/
	function SetDBField($name,$value)
	{
		$this->Records[$this->CurrentIndex][$name] = $value;
	}

	/**
	 * Apply where clause, that links this object to it's parent item
	 *
	 * @param string $special
	 * @access public
	 */
	function linkToParent($special)
	{
		$parent_prefix = $this->Application->getUnitOption($this->Prefix, 'ParentPrefix');
		if ($parent_prefix) {
			$parent_table_key = $this->Application->getUnitOption($this->Prefix, 'ParentTableKey');
			if (is_array($parent_table_key)) $parent_table_key = getArrayValue($parent_table_key, $parent_prefix);
			$foreign_key_field = $this->Application->getUnitOption($this->Prefix, 'ForeignKey');
			if (is_array($foreign_key_field)) $foreign_key_field = getArrayValue($foreign_key_field, $parent_prefix);

			if (!$parent_table_key || !$foreign_key_field) {
				return ;
			}

			$parent_object =& $this->Application->recallObject($parent_prefix.'.'.$special);
			/* @var $parent_object kDBItem */

			if (!$parent_object->isLoaded()) {
				$this->addFilter('parent_filter', 'FALSE');
				trigger_error('Parent ID not found (prefix: "<strong>' . rtrim($parent_prefix.'.'.$special, '.') . '</strong>"; sub-prefix: "<strong>' . $this->getPrefixSpecial() . '</strong>")', E_USER_NOTICE);
				return ;
			}

			// only for list in this case
			$parent_id = $parent_object->GetDBField($parent_table_key);
			$this->addFilter('parent_filter', '`' . $this->TableName . '`.`' . $foreign_key_field . '` = ' . $this->Conn->qstr($parent_id));
		}
	}

	/**
	 * Returns true if list was queried (same name as for kDBItem for easy usage)
	 *
	 * @return bool
	 */
	function isLoaded()
	{
		return $this->Queried && !$this->EOL();
	}

	/**
	 * Returns specified field value from all selected rows.
	 * Don't affect current record index
	 *
	 * @param string $field
	 * @param bool $formatted
	 * @param string $format
	 * @return Array
	 */
	function GetCol($field, $formatted = false, $format = null)
	{
		$i = 0;
		$ret = Array ();

		if ($formatted && array_key_exists('formatter', $this->Fields[$field])) {
			$formatter =& $this->Application->recallObject($this->Fields[$field]['formatter']);
			/* @var $formatter kFormatter */

			while ($i < $this->SelectedCount) {
				$ret[] = $formatter->Format($this->Records[$i][$field], $field, $this, $format);
				$i++;
			}
		}
		else {
			while ($i < $this->SelectedCount) {
				$ret[] = $this->Records[$i][$field];
				$i++;
			}
		}

		return $ret;
	}

	/**
	 * Set's field error, if pseudo passed not found then create it with message text supplied.
	 * Don't owerrite existing pseudo translation.
	 *
	 * @param string $field
	 * @param string $pseudo
	 * @param string $error_label
	 */
	function SetError($field, $pseudo, $error_label = null, $error_params = null)
	{
		$error_field = isset($this->Fields[$field]['error_field']) ? $this->Fields[$field]['error_field'] : $field;
		$this->FieldErrors[$error_field]['pseudo'] = $pseudo;

		$var_name = $this->getPrefixSpecial().'_'.$field.'_error';
		$previous_pseudo = $this->Application->RecallVar($var_name);

		if ($previous_pseudo) {
			// don't set more then one error on field
			return false;
		}

		$this->Application->StoreVar($var_name, $pseudo);

		return true;
	}

}