function ScaffoldForm(config) {

	var $this = this;

	$this.config = {
		'modelClass': '',
		'initializeMultipleEdit': false,
		'initializeSet': false,
		'translations': {
			'not_saved': 'Warning:\nYour changes have not been saved.',
			'ajax_save_error': 'Warning:\nYour changes have not been saved.',
			'ajax_success_message': 'The record has been successfully saved',
			'expand': 'Expand',
			'colapse': 'Colapse',
			'remove': 'Remove',
		}
	};

	$this.config = $.extend($this.config, config);
	$this.ajax = false;
	$this.saving = false;

	var $initialize = function () {

		$initializeMultipleEdit();

		$initializeSet();

		$handleAjaxSave();

		$handleCodemirror();

		$handleGeneralUIItems();

		$handlePageSelect();

		$handleValidationMessages();

		$handlePolyglotTabs();

		$(window).on('vector.set.reinit', function () {
			$initializeSet(true);
		});
	};


	// Initialize multiple edit form
	var $initializeMultipleEdit = function () {
		if (!$this.config.initializeMultipleEdit) return;

		$('.is-translated').attr('checked', true).css('opacity', 0.3);
		$('.is-translated').change(function () {
			$(this).prev().prev().val(1);
			var hidden = $(this).prev().prev();
			if (!$(this).is(':checked') && hidden.val() == 1 && $(this).css('opacity') != 1) {
				$(this).attr('checked', true).css('opacity', 1);
				hidden.val(1);

			} else if (!$(this).is(':checked') && hidden.val() == 1 && $(this).css('opacity') == 1) {
				$(this).css('opacity', 1);
				hidden.val(1);

			} else {
				$(this).css('opacity', 0.3);
				hidden.val(0);
			}
		});
	};


	// Handle set fields
	var $initializeSet = function (override) {
		if (!override && !$this.config.initializeSet) return;

		$('div.input.select.set select:not(.setter)').change(function () {
			if (!$(this).val()) $(this).parent().remove();
		});
		$('div.input.select.set select.setter').change(function () {
			if ($(this).val()) {
				var value = $(this).val();
				var sortableElement = $('<li>');
				$(this).val(0).clone()
					.val(value)
					.change(function () {
						if (!$(this).val()) $(this).parent().remove();
					})
					.appendTo(sortableElement);

				$(this).parent().find('ul').append(sortableElement);
			}
		});
	};

	// Save with Ctrl+S
	var $handleAjaxSave = function () {
		$(document).keydown(function (e) {
			if (e.ctrlKey && e.keyCode == 83) {
				$ajaxSave();
				return false;
			}
		});

		$(document).ready(function () {
			$('Form#' + $this.config.modelClass + 'FormForm').submit(function () {
				if ($this.ajax) {
					$this.ajax = false;
					$.post(
						$(this).attr('action'),
						$(this).serialize(),
						function (response) {
							if (response) {
								alert($this.config.translations.ajax_save_error + '\n' + response);
							}
							alert($this.config.translations.ajax_success_message);
							$this.saving = false;
						}
					);
					return false;
				}

				return true;
			});
		});
	};


	// Ajax save function
	var $ajaxSave = function () {
		if (!$this.saving) {
			$this.ajax = true;
			$this.saving = true;
			$('Form#' + $this.config.modelClass + 'FormForm').submit();
		}
		return false;
	};


	// Initialize codemirror textareas
	var $handleCodemirror = function () {
		$('textarea.cm-css, textarea.cm-js, textarea.cm-full').each(function (i, area) {
			var type = $(area).attr('class').replace(/^.*cm-([a-z]+).*$/, '$1');
			var _parser = "parse" + type + ".js";
			var _stylesheet = "/plugins/codemirror/css/" + type + "colors.css";
			if (type == 'full') {
				_parser = ["parsexml.js", "parsexml.js", "parsecss.js", "parsejs.js", "parsehtml.js", "tokenizephp.js", "parsephp.js", "parsephphtmlmixed.js"];
				_stylesheet = ["/plugins/codemirror/css/xmlcolors.css", "/plugins/codemirror/css/jscolors.css", "/plugins/codemirror/css/csscolors.css", "/plugins/codemirror/css/phpcolors.css"];
			}
			var editor = CodeMirror.fromTextArea($(area).attr('id'), {
				height: "dynamic",
				path: "/plugins/codemirror/js/",
				tabMode: 'spaces',
				indentUnit: 4,
				saveFunction: $ajaxSave,
				parserfile: _parser,
				stylesheet: _stylesheet
			});
		});
	};


	// Handle page select
	var $handlePageSelect = function () {
		$('.page-select').change(function () {
			var page = $(this).val();
			if (page == '') {
				$(this).prev().val('');
			} else {
				$(this).prev().val('page:' + page);
			}
		});
	};

	// Handle general ui elements like datepicker, sorting...
	var $handleGeneralUIItems = function () {
		$('.selectable .input-field').find('input, texteto area, select, .other').change(function () {
			$(this).closest('.selectable').find('span.label :checkbox').attr('checked', true);
		});

		/* Date picker */
		$('.date-time-picker').datetimepicker({
			showSecond: true,
			dateFormat: 'yy-mm-dd',
			timeFormat: 'HH:mm:ss',
			stepHour: 1,
			stepMinute: 1,
			stepSecond: 1,
			addSliderAccess: true,
			sliderAccessArgs: {touchonly: false}
		});

		/* Sorting for related models */
		$('.related > ul').sortable({
			delay: 200,
			axis: 'y',
			placeholder: 'ui-state-highlight'
		});

		/* Sorting for uploads */
		$('.upload-sortable').sortable({
			delay: 200,
			placeholder: 'ui-state-highlight',
			handle: '.vector-upload-drag'
		});

		/* Some tabs */
		$('.nav-tabs a').click(function () {
			$('.tab').hide();
			$($(this).attr('href')).show();
			$(this).closest('li').addClass('active').siblings().removeClass('active');
			$('.add-field').toggle($('#FieldTab').is(':visible'));
			return false;
		});
		$('.nav-tabs:not(.polyglot-tabs) a:first').click();

		// If specific tab
		if (window.location.hash) {
			var element = $('.nav-tabs a[href="' + window.location.hash + '"]');
			if (element.length) {
				element.trigger('click');
			}
		}

		/* GPS locaton dual value */
		$('[rel=gps_location]').change(function (event) {
			if ($(this).val().match(/^-?\d+(\,\d+)? +-?\d+(\,\d+)$/)) {
				$(this).val($(this).val().replace(/,/g, '.').replace(/ +/g, ','));
			}
		});
	};


	// Interconnecting with other modules
	window.InputFieldParseCoutner = {};
	window.InputFieldParser = {
		list: null,

		initialized: false,

		init: function () {
			if (this.initialized) {
				return;
			}
			this.initializeSearchField();
			this.initialized = true;
		},

		initializeSearchField: function () {
			$('.related .related-filter').each(function () {
				var list = $(this).closest('.related').find('ul').first();
				$(this).focus().select().bind('input propertychange', function () {

					// When value of the input is not blank
					if ($(this).val() !== '') {
						list.find('li').hide();
						list.find("li:contains-ci('" + $(this).val() + "')").show();
						// When there is no input or clean again, show everything back
					} else {
						list.find('li').show();
					}
				});

			});

		},


		// Get jQuery object from HTML
		parseTemplate: function (template) {
			return $(jQuery.parseHTML('<div>' + template + '</div>')[0]);
		},

		expandCollapseItem: function (item) {
			var area = item.find('.expandable-area');
			var button = item.find('.expand-collapse-related');

			if (area.is(':visible')) {
				var text = null;
				if (area.find('input:visible:first').length) {
					text = area.find('input:visible:first').val();
				} else {
					text = area.find('select option:selected').text();
				}
				item.find('.short-description').text(text);
				button.text(button.data('expand-text'));
			} else {
				item.find('.short-description').text('');
				button.text(button.data('collapse-text'));
			}
			area.toggle();
		},

		// Return references to elements inside HTML
		add: function (selector, fields, values, uploads) {
			if (values === null) values = {};

			var self = this;
			var dom = {};
			var item = $('<li>').addClass('related-item');
			var list = $(selector);

			var actions = $('<div class="clearfix">');
			actions.append($('<div class="short-description">'));

			// Remove button
			var remove = $('<a href="javascript:void(0)" class="btn btn-sm btn-danger pull-right btn-error remove-related">').html('<i class="fa fa-remove"></i> ' + $this.config.translations.remove);
			remove.click(function () {
				$(this).closest('li').remove();
			});
			remove.appendTo(actions);

			// Expand/collapse button
			var expand = $('<a href="javascript:void(0)" class="btn btn-sm btn-default pull-right expand-collapse-related" data-expand-text="Expand" data-collapse-text="Collapse">').html('<i class="fa fa-size"></i>');
			expand.click(function () {
				self.expandCollapseItem($(this).closest('li'));
			});
			expand.appendTo(actions);

			// Append actions
			actions.appendTo(item);

			// Append all other elements to expandable-area
			var div = $('<div class="expandable-area">');

			// Over each input field
			$.each(fields, function (name, template) {

				// Parse the template and append it to the list item
				template = template.replace(/###/g, list.children().length + 1);
				template = InputFieldParser.parseTemplate(template);
				template.attr('data-field-name', name);
				div.append(template);

				// Append to our list
				dom[name] = template.find('[id$=' + name + ']');
			});

			// Apply values
			for (var rel in values) {
				var inputElement = $('[rel=' + rel + ']', div);
				if (inputElement.is(':checkbox') && values[rel] === 1) {
					inputElement.attr('checked', 'checked');
				} else {
					inputElement.val(values[rel]);
				}
			}

			if (values) {
				if (uploads && uploads[values['id']]) {
					$.each(uploads[values['id']], function (association, html) {
						div.find('[data-field-name="' + association + '"]').html(html);
					});
				}

				if (!$.isEmptyObject(values)) {
					expand.text(expand.data('expand-text'));
					var text = div.find('input').filter(function () {
						return $(this).prop('type') === 'text' && $(this).val().trim().length > 0;
					}).val();

					if (!text) {
						text = div.find('select option:selected').text();
					}
					item.find('.short-description').text(text);
					div.hide();
				} else {
					expand.text(expand.data('collapse-text'));
				}
			}

			// Append to the list
			item.append(div);
			$(list).append(item);

			// $handleFileUpload();

			$(window).trigger('vector.set.reinit');

			div.find('[data-set-field]').each(function () {
				var $selectSet = $(this);
				var setField = $(this).attr('data-set-field');
				if (values[setField]) {
					$.each(values[setField], function (index, val) {
						$selectSet.val(val).trigger('change');
					})
				}
			});

			/* Sorting for related models */
			div.find('[data-set-field]').closest('.set').find('ul').sortable({
				delay: 200,
				axis: 'y',
				placeholder: 'ui-state-highlight'
			});

			return dom;
		}
	};


	// Handle tabs on validation error
	var $handleValidationMessages = function () {
		var $firstErrorMessage = $('.form-input-error-message').first();

		if ($firstErrorMessage.length > 0 && !$firstErrorMessage.is(':visible')) {
			var classes = $firstErrorMessage.parents().closest('.input').attr('class');
			var matches = classes.match(/polyglot-([a-z]*)/);

			if (matches) {
				$('.polyglot-tabs a[rel="' + matches[1] + '"]').trigger('click');
			}

			if ($('.nav-tabs').length > 0) {
				$('.nav-tabs li a[href="#' + $firstErrorMessage.parents().closest('.tab').attr('id') + '"]').trigger('click')
			}
		}
	};

	// Set item as translated on input keyup
	var $handlePolyglotTabs = function () {
		if ($('.polyglot-tabs').length > 0) {
			$('#content form').find(':input').each(function () {
				var $formItem = $(this);
				$formItem.keyup(function () {
					var className = $formItem.parents().closest('.input').attr('class');
					if (className) {
						var matches = className.match(/polyglot-([a-z]*)/);
						if (matches) {
							$('.polyglot-tabs a[rel="' + matches[1] + '"]').find('input[type="checkbox"]').prop('checked', true);
						}
					}
				});
			});
		}
	};

	// Case insensitive contains
	$.extend($.expr[":"], {
		"contains-ci": function (elem, i, match, array) {
			return (elem.textContent || elem.innerText || $(elem).text() || "").toLowerCase().indexOf((match[3] || "").toLowerCase()) >= 0;
		}
	});


	// Initialize script
	$initialize();
}


/**
 * Manager for the relationship forms.
 *
 * @param {string} module The module for which this relationship is being edited.
 * @param {string} relationship The module for which this relationship is being edited.
 * @param {Array[]} records The list of initial records.
 * @param {Array} options The options to use.
 *
 * @constructor
 */
RelationshipForm = function (module, relationship, records, options) {
	var self = this;

	/** @var {string} The module for which this relationship is being edited. */
	self.module = module;

	/** @var {string} The name of the relationship to use. */
	self.relationship = relationship;

	/** @var {Array} The options to use. */
	self.options = options;

	/** @var {Array[]} The list of records. */
	self.records = records;

	/** @var {Object} The index of all DOM elements. */
	self.dom = {

		/** @var {jQuery} The top element. */
		top: null,

		/** @var {jQuery} The element showing the list of selected elements. */
		list: null,

		/** @var {Object} The dialog showing the filter and a list of records to add. */
		dialog: {

			/** @var {jQuery} The element showing filter options in the search dialog. */
			filter: null,

			/** @var {jQuery} The element showing that there are no records to show. */
			empty: null,

			/** @var {jQuery} The element informing the user that the loading is in progress. */
			loading: null,

			/** @var {jQuery} The element showing the list of elements to add. */
			list: null,

			/** @var {jQuery} The button which resets the filter. */
			reset: null,

			/** @var {jQuery} The pagination controls. */
			pagination: null,

			/** @var {jQuery} The pagination Items. */
			paginationItems: null,

			/** @var {jQuery} The button which closes the dialog. */
			button: null
		}
	};

	/**
	 * Initialize the class.
	 */
	self.init = function () {

		// Load the main DOM elements
		self.dom.top = $('#' + self.module + '-Relationship-' + self.relationship);
		self.dom.list = self.dom.top.find('.relationship-form-selected-list');

		// Load the dialog DOM elements
		self.dom.dialog.top = $('#' + self.relationship + 'RelationshipDialog-Add');
		self.dom.dialog.filter = self.dom.dialog.top.find('.relationship-form-dialog-filter');
		self.dom.dialog.empty = self.dom.dialog.top.find('.relationship-form-dialog-empty');
		self.dom.dialog.loading = self.dom.dialog.top.find('.relationship-form-dialog-loading');
		self.dom.dialog.list = self.dom.dialog.top.find('.relationship-form-dialog-list');
		self.dom.dialog.reset = self.dom.dialog.top.find('.relationship-form-dialog-reset');
		self.dom.dialog.button = self.dom.dialog.top.find('.relationship-form-dialog-button');
		self.dom.dialog.pagination = self.dom.dialog.top.find('.relationship-form-dialog-pagination');
		self.dom.dialog.paginationItems = self.dom.dialog.pagination.find('.paginator');

		// Add the marker showing that we edited this page
		self.dom.top.prepend($('<input>').attr({
			name: 'data[' + self.module + '][Relationships][]',
			type: 'hidden',
			value: self.relationship
		}));

		// Reset filter
		self.dom.dialog.reset.click(function () {
			self.dom.dialog.filter.find('input, textarea, select').val('');
			self.search(1);
		});

		// Add to selected list
		self.dom.dialog.list.on('click', '[data-relationship-action="add"]', function () {
			self.add($(this).closest('[data-record]').data('record'));
		});

		// Remove from selected list
		self.dom.dialog.list.on('click', '[data-relationship-action="remove"]', function () {
			var record = $(this).closest('[data-record]').data('record');
			for (var i in self.records) {
				if (self.records.hasOwnProperty(i) && self.records[i].slave_fk === record.slave_fk) {
					self.records.splice(i, 1);
					self.refresh();
					return;
				}
			}
		});

		// Build the filter and load initial data
		self.buildFilter();
		self.search(1);

		// Refresh the initial view
		self.refresh();
	};

	/**
	 * Execute the search, with optional conditions.
	 *
	 * @param {int} page The page for pagination, starting from 1.
	 */
	self.search = function (page) {

		// Show loader
		self.dom.dialog.list.hide();
		self.dom.dialog.empty.hide();
		self.dom.dialog.button.hide();
		self.dom.dialog.pagination.hide();
		self.dom.dialog.loading.show();

		// Get conditions
		var conditions = {};
		self.dom.dialog.filter.find('input, textarea, select').each(function (i, input) {
			input = $(input);
			if (input.val() !== '') {
				conditions[input.attr('name')] = input.val();
			}
		});

		// Define POST data
		var post = {
			conditions: conditions ? conditions : {},
			page: page ? Math.max(1, page) : {}
		};

		// Load
		$.post('/module-relationships/' + self.module + '/' + self.relationship, post, function (response) {
			self.dom.dialog.loading.hide();
			self.dom.dialog.button.show();

			// If there are results
			if (response.records.length) {
				self.dom.dialog.list.html('').show();

				// Pagination
				if (response.page.total > 1) {
					self.dom.dialog.pagination.show();
					self.dom.dialog.paginationItems.html('');

					// Show numbers
					var page = 0;

					while (++page <= response.page.total) {
						var link = $('<a href="javascript:void(0)" class="' + (page === response.page.current ? 'active' : '') + '">').text(page);
						self.dom.dialog.paginationItems.append(link.click(function () {
							self.search($(this).text());
						}));
					}
				}

				// Print received objects
				for (var i in response.records) {
					if (response.records.hasOwnProperty(i)) {

						// Create data
						var data = {
							record: response.records[i]
						};

						// Create item
						var item = hbs(self.options.template.add, data);
						item.data('record', data.record);

						self.dom.dialog.list.append(item);
					}
				}

				// Refresh
				self.refresh();

				// Inform that there are no records available
			} else {
				self.dom.dialog.empty.show();
			}
		});
	};

	/**
	 * Builds the filter in the dialog.
	 */
	self.buildFilter = function () {

		// Initial term
		var list = [$('<div class="input">').append($('<label>').text('Search by keyword or term')).append($('<input name="_term">'))];

		// Additional filters
		for (var name in self.options.filter) {
			if (self.options.filter.hasOwnProperty(name)) {
				var filter = self.options.filter[name];

				// Create select and add blank option
				var select = $('<select name="' + filter.field + '">');
				select.append($('<option value="">').text(''));

				// Add options
				for (var value in filter.options) {
					if (filter.options.hasOwnProperty(value)) {
						select.append($('<option value="' + value + '">').text(filter.options[value]))
					}
				}

				// Add label
				var label = $('<label>').text(name);
				var inputWrapper = $('<div class="input">').append(label).append(select);
				list.push(inputWrapper);
			}
		}

		// Add all items to the filter
		var width = Math.floor(12 / list.length);
		for (var i in list) {
			if (list.hasOwnProperty(i)) {
				var input = list[i].find('input, textarea, select');

				// Append to the filter
				self.dom.dialog.filter.append($('<div class="relationship-form-dialog-filter-item col-md-' + width + '">').append(list[i]));

				// Attach the on change listener
				input.data('action', function () {
					self.search(1);
				});

				// Special case for inputs
				if (input.is('input')) {
					input.keyup(function (event) {
						var element = $(event.currentTarget);

						// Do not execute if no changes
						if (element.data('last') !== element.val()) {

							// Allow a window of 750 milliseconds to type the next character
							clearTimeout(element.data('todo'));
							element.data('todo', setTimeout(function () {
								element.data('action')();
								element.data('last', element.val())
							}, 750));
						}
					});

				} else {
					input.change(input.data('action'));
				}
			}
		}
	};

	/**
	 * Add a new record to the selected list.
	 *
	 * @param {Array} record The record to add.
	 *
	 * @return {boolean} True if the item was added, false if it was already selected.
	 */
	self.add = function (record) {

		// Make sure the record is no already on the list
		for (var i in self.records) {
			if (self.records.hasOwnProperty(i) && self.records[i]['slave_fk'] === record['slave_fk']) {
				return false;
			}
		}

		// Add to the list
		self.records.push(record);
		self.refresh();
		return true;
	};

	/**
	 * Refresh the view.
	 */
	self.refresh = function () {

		// Mark all as not selected
		self.dom.dialog.list.children().removeClass('selected');

		// Remove all views
		self.dom.list.html('');

		// Add for each record
		self.records.forEach(function (record, index, records) {

			// Get the id
			var id = record.Relationship && record.Relationship.id
				? record.Relationship.id
				: Math.round(Math.random() * 1e15);

			// Get data for the element
			var data = {
				index: index,
				record: record,
				records: records,

				// Relationship info
				relationship: record.Relationship,
				inputPrefix: 'data[' + self.module + '][Relationship][' + self.relationship + '][' + id + ']'
			};

			// Create element and add id of the related model as the hidden input
			var element = hbs(self.options.template.selected, data);
			element.prepend($('<input>').attr({
				name: data.inputPrefix + '[slave_fk]',
				type: 'hidden',
				value: record.slave_fk
			}));

			// Action: Remove
			element.find('[data-relationship-action="remove"]')
				.css({cursor: "pointer"})
				.click(function () {
					self.records.splice(index, 1);
					self.refresh();
				});

			// Show element
			self.dom.list.append(element);

			// Mark as added in the dialog list
			self.dom.dialog.list.children().each(function (index, row) {
				row = $(row);
				if (row.data('record').slave_fk === record.slave_fk) {
					row.addClass('selected');
				}
			});
		});
	};

	// Initialize
	self.init();
};
