function MultiInputControl($field_name, $field_mask, $field_labels, $result_mask) {
	this.FieldName = $field_name;
	this.ValidateURL = '';

	this.FieldMask = $field_mask;
	this.FieldLabels = $field_labels;
	this.ResultMask = $result_mask; // format of record in list

	this.Permissions = new Array (); // action groups allowed
	this.Messages = new Array (); // various phrase (errors, confirmations, button titles)
	this.Controls = {}; // controls used for editing list content
	this.Records = new Array (); // data to be submitted (needs to be parsed using formatters)
	this.Errors = {}; // error messages in fields
	this.InEditing = false;
}

MultiInputControl.prototype.registerControl = function($field_name, $type, $required, $options) {
	if (Object.prototype.toString.call($type) === "[object String]") {
		this.Controls[$field_name] = {
			'type' : $type,
			'required' : $required,
			'options' : $options
		};
	}
	else {
		if ($required === undefined || $required === false) {
			// type, required, options, default
			this.Controls[$field_name] = $type;
		}
		else {
			// upgrade control
			$.extend(this.Controls[$field_name], $type);
		}
	}
}

MultiInputControl.prototype.getControl = function ($field, $appendix, $prepend) {
	$appendix = isset($appendix) ? '_' + $appendix : '';
	$prepend = isset($prepend) ? $prepend + '_' : '';

	return document.getElementById( $prepend + this.FieldMask.replace('#FIELD_NAME#', $field) + $appendix );
}

MultiInputControl.prototype.getControlValue = function ($field) {
	var $value = '';

	switch (this.Controls[$field]['type']) {
		case 'select':
			var $control = this.getControl($field);
			$value = $control.options[$control.selectedIndex].value;
			break;

		case 'radio':
			$value = $("input[name='" + jq(this.FieldMask.replace('#FIELD_NAME#', $field)) + "']:checked").val();
			break;

		/*case 'datetime':
			$value = this.getControl($field + '_date').value + ' ' + this.getControl($field + '_time').value;
			break;*/

		default:
			$value = this.getControl($field).value;
			break;
	}

	return $value;
}

MultiInputControl.prototype.setControlValue = function ($field, $value) {
	if ($value === null && this.Controls[$field]['default'] !== undefined) {
		$value = this.Controls[$field]['default'];
	}

	switch (this.Controls[$field]['type']) {
		case 'select':
			var $i = 0;
			var $control = this.getControl($field);
			if ($value === null) {
				$control.selectedIndex = 0;
			}
			else {
				$($control).val($value).change();
			}
			break;

		case 'checkbox':
			this.getControl($field).value = ($value === null) ? 0 : $value;
			this.getControl($field, null, '_cb').checked = parseInt($value) == 1;
			break;

		case 'radio':
			if ($value === null) {
				$("input[name='" + jq(this.FieldMask.replace('#FIELD_NAME#', $field)) + "']:checked").attr('checked', '').change();
			}
			else {
				$( this.getControl($field, $value) ).attr('checked', 'checked').change();
			}
			break;

		/*case 'datetime':
			$value = $value.split(' ');
			this.getControl($field + '_date').value = $value[0];
			this.getControl($field + '_time').value = $value[1];
			break;*/

		default:
			var $control = this.getControl($field);
			$($control).val($value === null ? '' : $value).change();
			break;
	}
}

MultiInputControl.prototype.formatValue = function ($field, $value) {
	if (this.Controls[$field]['type'] == 'select') {
		var $i = 0;
		var $control = this.getControl($field);
		while ($i < $control.options.length) {
			if ($control.options[$i].value == $value) {
				$value = $control.options[$i].innerHTML;
				break;
			}
			$i++;
		}

	}

	if (this.Controls[$field]['type'] == 'textbox') {
		var $field_options =  this.Controls[$field]['options'];
		if ($field_options && parseInt($field_options.first_chars) > 0) {
		 	$value = $value.substring(0, parseInt($field_options.first_chars));
		 }
	}

	if (this.Controls[$field]['type'] == 'checkbox' || this.Controls[$field]['type'] == 'radio') {
		$value = this.Controls[$field]['options'][$value];
	}

	return $value;
}

MultiInputControl.prototype.formatLine = function($record_index, $escape) {
	var $ret = this.ResultMask;

	for (var $field_name in this.Controls) {
		var $value = this.Records[$record_index][$field_name];
		$ret = $ret.replace('#' + $field_name + '#', this.formatValue($field_name, $value));
	}

	return $escape === undefined || $escape ? this.htmlspecialchars($ret) : $ret;
}

MultiInputControl.prototype.isReadOnly = function($record_index) {
	return false;
}

MultiInputControl.prototype._getRecordIndex = function ($selected_index) {
	var $object = this.getControl(this.FieldName, 'minput');

	if (!isset($selected_index)) {
		$selected_index = $object.selectedIndex;
	}

	return $selected_index == -1 ? -1 : $object.options[$selected_index].value;
}

MultiInputControl.prototype.makeRequest = function($request_type, $record, $skip_index) {
	var $url = this.ValidateURL + '&request_type=' + encodeURIComponent($request_type);

	for (var $field_name in $record) {
		$url += '&' + this.FieldMask.replace('#FIELD_NAME#', $field_name) + '=' + encodeURIComponent($record[$field_name]);
	}

	Request.makeRequest($url, this.BusyRequest, '', this.successCallback, this.errorCallback, [$request_type, $record, $skip_index], this);
}

MultiInputControl.prototype.AddRecord = function() {
	var $record = this.prepareRecord();

	if (this.InEditing) {
		// already in editing
		var $record_index = this.getControl(this.FieldName, 'minput').selectedIndex;
		this.makeRequest('SaveRecord', $record, $record_index);
		return ;
	}

	if (this.hasPermission('add')) {
		this.makeRequest('AddRecord', $record, false);
	}
}

MultiInputControl.prototype.EditRecord = function() {
	var $record_index = this._getRecordIndex(); // this.getControl(this.FieldName, 'minput').selectedIndex;
	if ($record_index == -1 || this.InEditing) {
		// no record selected
		return ;
	}

	this.InEditing = true;
	var $edit_record = this.Records[$record_index];

	for (var $field_name in $edit_record) {
		this.setControlValue($field_name, $edit_record[$field_name]);
	}

	this.getControl(this.FieldName, 'add_button').value = this.Messages['save_button'];
	this.getControl(this.FieldName, 'minput').disabled = true;

	this.SetButtonState('edit', false);
	this.SetButtonState('delete', false);
}


MultiInputControl.prototype.ResetControls = function() {
	for (var $field_name in this.Controls) {
		this.setControlValue($field_name, null);
	}

	this.Errors = {};
}

MultiInputControl.prototype.CancelEditing = function() {
	this.ResetControls();

	this.getControl(this.FieldName, 'add_button').value = this.Messages['add_button'];
	this.getControl(this.FieldName, 'minput').disabled = false;

	this.SetButtonState('edit', true);
	this.SetButtonState('delete', true);
	this.InEditing = false;
}

MultiInputControl.prototype.ShowRecord = function($option_index) {
	var $options = this.getControl(this.FieldName, 'minput').options;

	if ($option_index < $options.length) {
		// update existing record
		$options[$option_index].innerHTML = this.formatLine( this._getRecordIndex($option_index) );
		$options[$option_index].disabled = this.isReadOnly( this._getRecordIndex($option_index) );
	}
	else {
		// create new record
		var $new_option = document.createElement('OPTION');
		$options.add($new_option, $options.length);
		$new_option.value = $option_index; // will be used in move up/down & sorting (if any)
		$new_option.innerHTML = this.formatLine(this.Records.length - 1);
		$new_option.disabled = this.isReadOnly(this.Records.length - 1);
	}
}

MultiInputControl.prototype.DeleteRecords = function() {
	if (!confirm(this.Messages['delete_confirm'])) {
		return ;
	}

	var $control = this.getControl(this.FieldName, 'minput');
	var $i = $control.length - 1;
	while ($i >= 0) {
		if ($control.options[$i].selected == true) {
			this.Records[$control.options[$i].value] = null; // preserves index, when removing element from middle of array.  this.Records.splice($control.options[$i].value, 1);
			$control.remove($i);
		}
		$i--;
	}

	this.SaveValues();
}

MultiInputControl.prototype.MoveRecordsUp = function() {
	move_options_up(this.getControl(this.FieldName, 'minput'), 1);
	this.SaveValues();
}

MultiInputControl.prototype.MoveRecordsDown = function() {
	move_options_down(this.getControl(this.FieldName, 'minput'), 1);
	this.SaveValues();
}

MultiInputControl.prototype.AddFromXML = function($xml) {
	var $document = getDocumentFromXML($xml);
	this.ProcessXMLNode($document);
}

MultiInputControl.prototype.ProcessXMLNode = function($node, $root_name) {
	for (var $i = 0; $i < $node.childNodes.length; $i++) {
		var $child = $node.childNodes.item($i);
		if ($child.tagName == 'record') {
			this.Records[this.Records.length] = {};
			this.ProcessXMLNode($child, $root_name);
			this.ShowRecord(this.Records.length - 1);
		}
		else if ($child.tagName == 'field') {
			if ($root_name == 'records') {
				// no firstChild, when node value is empty!
				this.Records[this.Records.length - 1][$child.getAttribute('name')] = $child.firstChild ? $child.firstChild.nodeValue : '';
			}
			else if ($root_name == 'errors') {
				this.Errors[$child.getAttribute('name')] = $child.firstChild.nodeValue;
			}
		}
		else if ($child.tagName == 'records') {
			this.ProcessXMLNode($child, $child.tagName);
		}
		else if ($child.tagName == 'errors') {
			this.Errors = {};
			this.ProcessXMLNode($child, $child.tagName);
		}
	}
}

MultiInputControl.prototype.LoadValues = function() {
	var $current_value = this.getControl(this.FieldName).value;
	if ($current_value) {
		this.AddFromXML($current_value);
	}
}

MultiInputControl.prototype.SaveValues = function() {
	var $object = this.getControl(this.FieldName, 'minput');
	var $record_index = 0;
	var $xml = '';

	var $i = 0;
	while ($i < $object.options.length) {
		$record_index = $object.options[$i].value;
		$xml += '<record>';
		for (var $field_name in this.Controls) {
			$xml += '<field name="' + $field_name + '">' + this.htmlspecialchars(this.Records[$record_index][$field_name]) + '</field>';
		}
		$xml += '</record>';
		$i++;
	}

	this.getControl(this.FieldName).value = $xml ? '<records>' + $xml + '</records>' : '';
}

MultiInputControl.prototype.htmlspecialchars = function (string) {
	string = string.toString();
	string = string.replace(/&/g, '&amp;');
	string = string.replace(/</g, '&lt;');
	string = string.replace(/>/g, '&gt;');
	string = string.replace(/\"/g, '&quot;');

	return string;
}

MultiInputControl.prototype.prepareRecord = function() {
	var $record = {};
	for (var $field_name in this.Controls) {
		$record[$field_name] = this.getControlValue($field_name);
	}
	return $record;
}

MultiInputControl.prototype.ValidateRecord = function($record, $skip_index) {
	var $valid = true;

	$valid = $valid && this.ValidateRequired($record);
	$valid = $valid && this.ValidateUnique($record, $skip_index);

	return $valid;
}

MultiInputControl.prototype.ValidateRequired = function($record) {
	for (var $field_name in $record) {
		if (this.Controls[$field_name]['required'] && !$record[$field_name]) {
			this.Errors[$field_name] = this.Messages['required_error'];
		}
	}

	return !this.displayErrors();
}

MultiInputControl.prototype.compareRecords = function($record_a, $record_b) {
	var $equals = true;

	for (var $field_name in $record_a) {
		if ($record_a[$field_name] !== $record_b[$field_name]) {
			return false;
		}
	}

	return $equals;
}

MultiInputControl.prototype.ValidateUnique = function($record, $skip_index) {
	var $i = 0;
	if (!isset($skip_index)) {
		$skip_index = -1;
	}

	while ($i < this.Records.length) {
		if (this.Records[$i] == null) {
			// skip deleted records
			$i++;
			continue;
		}

		if ($i != $skip_index && this.compareRecords($record, this.Records[$i])) {
			alert(this.Messages['unique_error']);
			return false;
		}
		$i++;
	}

	return true;
}

MultiInputControl.prototype.displayErrors = function() {
	var $ret = [];

	for (var $field_name in this.Errors) {
		$ret.push(this.FieldLabels[$field_name] + ': ' + this.Errors[$field_name]);
	}

	if ($ret.length) {
		alert( $ret.join("\n") );
		return true;
	}

	return false;
}

MultiInputControl.prototype.successCallback = function($request, $params, $object) {
	if (Request.processRedirect($request) === true) {
		return ;
	}

	var $document = getDocumentFromXML($request.responseText);
	$object.ProcessXMLNode($document);

	if ($object.displayErrors()) {
		return ;
	}

	// params: 0 - action type, 1 - record data, 2 - option index
	switch ($params[0]) {
		case 'AddRecord':
			if (!$object.ValidateRecord($params[1])) {
				return ;
			}
			$object.Records.push($params[1]);

			$object.ShowRecord($object.Records.length - 1);
			$object.ResetControls();
			break;

		case 'SaveRecord':
			$record_index = $object._getRecordIndex($params[2]);
			if (!$object.ValidateRecord($params[1], $record_index)) {
				return ;
			}

			$object.Records[$record_index] = $params[1];
			$object.ShowRecord($params[2]);

			$object.CancelEditing();
			break;
	}

	$object.SaveValues();
}

MultiInputControl.prototype.errorCallback = function($request, $params, $object) {
	alert('AJAX Error; class: MultiInputControl; ' + Request.getErrorHtml($request));
}

MultiInputControl.prototype.SetMessage = function($pseudo, $message) {
	this.Messages[$pseudo] = $message;
}

MultiInputControl.prototype.InitEvents = function() {
	var $button = null;
	var $var_name = this.FieldName;

	$button = this.getControl(this.FieldName, 'add_button');
	$button.onclick = function() { eval($var_name).AddRecord() };

	if (this.hasPermission('add') || this.hasPermission('edit')) {
		$button = this.getControl(this.FieldName, 'cancel_button');
		$button.onclick = function() { eval($var_name).CancelEditing() };
	}

	if (this.hasPermission('edit')) {
		$button = this.getControl(this.FieldName, 'edit_button');
		$button.onclick = function() { eval($var_name).EditRecord() };

		$button = this.getControl(this.FieldName, 'minput');
		$button.ondblclick = function() { eval($var_name).EditRecord() };
	}

	if (this.hasPermission('delete')) {
		$button = this.getControl(this.FieldName, 'delete_button');
		$button.onclick = function() { eval($var_name).DeleteRecords() };
	}

	if (this.hasPermission('move')) {
		$button = this.getControl(this.FieldName, 'moveup_button');
		$button.onclick = function() { eval($var_name).MoveRecordsUp() };

		$button = this.getControl(this.FieldName, 'movedown_button');
		$button.onclick = function() { eval($var_name).MoveRecordsDown() };
	}
}

MultiInputControl.prototype.hasPermission = function ($perm_name) {
	return in_array($perm_name, this.Permissions);
}

MultiInputControl.prototype.SetPermission = function ($perm_name, $perm_value) {
	var $perm_index = array_search($perm_name, this.Permissions);

	if ($perm_index != -1) {
		// permission found
		if (!$perm_value) {
			this.Permissions = this.Permissions.splice($perm_index, 1);
		}
	}
	else if ($perm_value) {
		// permission not found
		this.Permissions.push($perm_name);
	}
}

MultiInputControl.prototype.SetButtonState = function ($button, $mode) {
	if (!this.hasPermission($button)) {
		return ;
	}

	var $button = this.getControl(this.FieldName, $button + '_button');

	$button.disabled = !$mode;
	$button.className = $mode ? 'button' : 'button-disabled';
}


// =======================================================================================

function EditPickerControl($field_name, $field_mask) {
	this.FieldName = $field_name;
	this.FieldMask = $field_mask;
	this.Messages = new Array ();

	this.InitEvents();
	select_sort( this.getControl('available') );
}

EditPickerControl.prototype.getControl = function ($type) {
	var $control_id = this.FieldMask + (isset($type) ? '_' + $type : '');
	return document.getElementById($control_id);
}

EditPickerControl.prototype.SetMessage = function ($pseudo, $message) {
	this.Messages[$pseudo] = $message;
}

EditPickerControl.prototype.SaveValues = function () {
	this.getControl().value = select_to_string(this.getControl('selected'));
	this.getControl('available_field').value = select_to_string(this.getControl('available'));
}

EditPickerControl.prototype.MoveLeft = function () {
	move_selected(this.getControl('available'), this.getControl('selected'), this.Messages['nothing_selected']);
	this.SaveValues();
}

EditPickerControl.prototype.MoveRight = function () {
	move_selected(this.getControl('selected'), this.getControl('available'), this.Messages['nothing_selected']);
	select_sort( this.getControl('available') );
	this.SaveValues();
}

EditPickerControl.prototype.InitEvents = function() {
	var $button = null;
	var $var_name = this.FieldName;

	$button = this.getControl('move_left_button');
	$button.onclick = function() { eval($var_name).MoveLeft() };

	$button = this.getControl('move_right_button');
	$button.onclick = function() { eval($var_name).MoveRight() };
}