/**
 * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
 */

/* global $ */
/* global EventEmitter */
/* @depend QfqEvents.js */
/* @depend ElementUpdate.js */
/* @depend Dirty.js */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};

(function (n) {
    'use strict';

    // TODO: This object is getting its own gravitational field. Start refactoring.
    /**
     * Represents a QFQ Form.
     *
     * QfqForm will autonomously fire an lock `extend` request when the lock expired, but the last change `t_c` has
     * been made during the lock period `t_l`. I.e. let `t_{current}` be the current time, an `extend` request is made
     * when
     *
     *    t_c + t_l > t_{current}
     *
     * holds.
     *
     * @param formId {string} value of the form's id attribute
     * @param submitTo {string} url where data will be submitted to
     * @param deleteUrl {string} url to call upon record deletion
     * @param dataRefreshUrl {string} url where to fetch new element values from
     * @param fileUploadTo {string} url used for file uploads
     * @param fileDeleteUrl {string} url used to delete files
     * @param dirtyUrl {string} url used to query
     * @constructor
     *
     * @name QfqNS.QfqForm
     */
    n.QfqForm = function (formId, submitTo, deleteUrl, dataRefreshUrl, fileUploadTo, fileDeleteUrl, dirtyUrl) {
        this.formId = formId;
        this.submitTo = submitTo;
        this.deleteUrl = deleteUrl;
        this.dataRefreshUrl = dataRefreshUrl;
        this.fileUploadTo = fileUploadTo;
        this.fileDeleteUrl = fileDeleteUrl;
        this.dirtyUrl = dirtyUrl;
        this.dirtyFired = false;
        this.lockAcquired = false;
        this.formImmutableDueToConcurrentAccess = false;
        this.lockRenewalPhase = false;
        this.goToAfterSave = false;
        this.skipRequiredCheck = false;
        this.activateFirstRequiredTab = true;

        this.additionalQueryParameters = {
            'recordHashMd5': this.getRecordHashMd5()
        };

        if (!!$('#' + QfqNS.escapeJqueryIdSelector(this.formId)).data('enable-save-button')) {
            this.form = new n.Form(this.formId, false);
            this.getSaveButton().removeClass("disabled").removeAttr("disabled");
        } else {
            this.getSaveButton().addClass("disabled").attr("disabled", "disabled");
            this.form = new n.Form(this.formId, false);
        }

        if ($('#' + QfqNS.escapeJqueryIdSelector(this.formId)).data('required-off-but-mark')) {
            this.skipRequiredCheck = true;
        } else {
            this.skipRequiredCheck = false;
        }

        if (typeof $('#' + QfqNS.escapeJqueryIdSelector(this.formId)).data('activate-first-required-tab') !== 'undefined') {
            this.activateFirstRequiredTab = $('#' + QfqNS.escapeJqueryIdSelector(this.formId)).data('activate-first-required-tab');
        }

        this.infoLockedButton = this.infoLockedButton.bind(this);

        // This is required when displaying validation messages, in order to activate the tab, which has validation
        // issues
        this.bsTabs = null;
        this.lastButtonPress = null;

        this.eventEmitter = new EventEmitter();

        this.dirty = new n.Dirty(this.dirtyUrl);
        this.dirty.on(n.Dirty.EVENTS.SUCCESS, this.dirtyNotifySuccess.bind(this));
        this.dirty.on(n.Dirty.EVENTS.DENIED, this.dirtyNotifyDenied.bind(this));
        this.dirty.on(n.Dirty.EVENTS.FAILED, this.dirtyNotifyFailed.bind(this));
        this.dirty.on(n.Dirty.EVENTS.SUCCESS_TIMEOUT, this.dirtyTimeout.bind(this));
        this.dirty.on(n.Dirty.EVENTS.RENEWAL_DENIED, this.dirtyRenewalDenied.bind(this));
        this.dirty.on(n.Dirty.EVENTS.RENEWAL_SUCCESS, this.dirtyRenewalSuccess.bind(this));


        this.form.on('form.changed', this.changeHandler.bind(this));
        this.form.on('form.reset', this.resetHandler.bind(this));
        this.form.on('form.submit.successful', this.submitSuccessDispatcher.bind(this));
        this.form.on('form.submit.failed', function (obj) {
            n.Helper.showAjaxError(null, obj.textStatus, obj.errorThrown);
        });

        this.getSaveButton().click(this.handleSaveClick.bind(this));
        this.getCloseButton().click(this.handleCloseClick.bind(this));
        this.getNewButton().click(this.handleNewClick.bind(this));
        this.getDeleteButton().click(this.handleDeleteClick.bind(this));

        var that = this;
        $('.external-save').click(function(e) {
            var uri = $(this).data('target');
            that.callSave(uri);
        });

        this.setupFormUpdateHandler();
        if (!!$('#' + QfqNS.escapeJqueryIdSelector(this.formId)).data('disable-return-key-submit')) {
            // Nothing to do
        } else {
            this.setupEnterKeyHandler();
        }

        this.fileUploader = new n.FileUpload('#' + this.formId, this.fileUploadTo);
        this.startUploadHandler = this.startUploadHandler.bind(this);
        this.fileUploader.on('fileupload.started', this.startUploadHandler);
        this.fileUploader.on('fileupload.upload.successful', that.fileUploadSuccessHandler);

        this.fileUploader.on('fileupload.upload.failed',
            function (obj) {
                n.Helper.showAjaxError(null, obj.textStatus, obj.errorThrown);
            });
        this.endUploadHandler = this.endUploadHandler.bind(this);
        this.fileUploader.on('fileupload.ended', this.endUploadHandler);

        this.fileDeleter = new n.FileDelete("#" + this.formId, this.fileDeleteUrl);
        this.fileDeleter.on('filedelete.delete.successful', this.fileDeleteSuccessHandler.bind(this));

        this.fileDeleter.on('filedelete.delete.failed',
            function (obj) {
                n.Helper.showAjaxError(null, obj.textStatus, obj.errorThrown);
            });

        var configurationData = this.readFormConfigurationData();
        this.applyFormConfiguration(configurationData);

        // Initialize jqxDateTimeInput elements.
        n.Helper.jqxDateTimeInput();
        // Initialize jqxComboBox elements.
        n.Helper.jqxComboBox();
        // Deprecated
        //n.Helper.jqxEditor();
        n.Helper.tinyMce();
        n.Helper.codemirror();
        
        this.form.on('form.submit.before', n.Helper.tinyMce.prepareSave);
        this.form.on('form.validation.before', n.Helper.tinyMce.prepareSave);
        this.form.on('form.validation.failed', this.validationError);
        this.form.on('form.validation.success', this.validationSuccess);

        $(".radio-inline").append($("<span>", { class: "checkmark", aria: "hidden"}));
        $(".checkbox-inline").append($("<span>", { class: "checkmark", aria: "hidden"}));
        $(".radio").append($("<span>", { class: "checkmark", aria: "hidden"}));
        $(".checkbox").append($("<span>", { class: "checkmark", aria: "hidden"}));

    };

    n.QfqForm.prototype.on = n.EventEmitter.onMixin;

    n.QfqForm.prototype.dirtyNotifySuccess = function (obj) {
        this.lockAcquired = true;
        // Intentionally empty. May be used later on.
    };

    n.QfqForm.prototype.dirtyRenewalSuccess = function (obj) {
        this.lockAcquired = true;
    };

    /**
     * @public
     */
    n.QfqForm.prototype.releaseLock = function (async) {
        if (!this.lockAcquired) {
            n.Log.debug("releaseLock(): no lock acquired or already released.");
            return;
        }
        n.Log.debug("releaseLock(): releasing lock.");
        this.dirty.release(this.getSip(), this.getRecordHashMd5AsQueryParameter(), async);
        this.resetLockState();
    };

    n.QfqForm.prototype.resetLockState = function () {
        this.dirty.clearSuccessTimeoutTimerIfSet();
        this.dirtyFired = false;
        this.formImmutableDueToConcurrentAccess = false;
        this.lockRenewalPhase = false;
        this.lockAcquired = false;
    };

    n.QfqForm.prototype.dirtyRenewalDenied = function (obj) {
        var that = this;
        var messageButtons = [{
            label: "Reload",
            eventName: 'reload'
        }];
        if (obj.data.status == "conflict_allow_force") {
            messageButtons.push({
                label: "Ignore",
                eventName: 'ignore'
            });
        }
        var alert = new n.Alert(
            {
                type: "error",
                message: obj.data.message,
                modal: true,
                buttons: messageButtons
            }
        );
        alert.on('alert.reload', function () {
            that.eventEmitter.emitEvent('qfqform.close-intentional', n.EventEmitter.makePayload(that, null));
            window.location.reload(true);
        });
        alert.on('alert.ignore', function () {
            console.log("Ignored Recordlock");
        });
        alert.show();
    };

    n.QfqForm.prototype.dirtyTimeout = function (obj) {
        this.dirtyFired = false;
        this.lockAcquired = false;
        this.lockRenewalPhase = true;

        // Figure out whether the user made changes in the lock timeout period
        if (this.form.formChangedTimestampInMilliseconds + this.dirty.lockTimeoutInMilliseconds >
            Date.now()) {
            // Renew without user intervention.
            this.fireDirtyRequestIfRequired();
            // and bail out
            return;
        }
        var alert = new n.Alert(
            {
                message: "Exclusive access to document timed out.",
                type: "warning"
            }
        );
        alert.show();
    };

    n.QfqForm.prototype.dirtyNotifyDenied = function (obj) {
        var messageType;
        var isModal = true;
        var messageButtons = [{
            label: "Reload",
            eventName: 'reload'
        }];
        var message;
        var that = this;

        switch (obj.data.status) {
            case "conflict":
                messageType = "error";
                this.setButtonEnabled(this.getSaveButton(), false);
                this.getSaveButton().removeClass(this.getSaveButtonAttentionClass());
                this.setButtonEnabled(this.getDeleteButton(), false);
                this.formImmutableDueToConcurrentAccess = true;
                this.lockAcquired = false;
                break;
            case "conflict_allow_force":
                messageType = "warning";
                messageButtons.push({
                    label: "Ignore",
                    eventName: 'ignore'
                });
                break;
            case "error":
                messageType = "error";
                this.setButtonEnabled(this.getSaveButton(), false);
                this.getSaveButton().removeClass(this.getSaveButtonAttentionClass());
                this.setButtonEnabled(this.getDeleteButton(), false);
                // Do not make the form ask for saving changes.
                this.form.formChanged = false;
                this.formImmutableDueToConcurrentAccess = true;
                this.lockAcquired = false;
                break;
            default:
                n.Log.error('Invalid dirty status: \'' + obj.data.status + '\'. Assume messageType \'error\'');
                messageType = "error";
                break;
        }

        message = new n.Alert({
            message: obj.data.message,
            type: messageType,
            timeout: n.Alert.constants.NO_TIMEOUT,
            modal: isModal,
            buttons: messageButtons
        });
        message.on('alert.reload', function () {
            that.eventEmitter.emitEvent('qfqform.close-intentional', n.EventEmitter.makePayload(that, null));
            window.location.reload(true);
        });
        message.show();
    };

    n.QfqForm.prototype.dirtyNotifyFailed = function () {
        this.dirtyFired = false;
        this.lockAcquired = false;
    };

    n.QfqForm.prototype.validationError = function (info) {
        var $formControl = $(info.data.element);
        var $messageContainer = $formControl.siblings('.hidden.with-errors');

        if ($messageContainer.length === 0) {
            if ($formControl.parent().hasClass('input-group')) {
                $messageContainer = $formControl.parent().siblings('.hidden.with-errors');
            }
        }

        $messageContainer.data('qfq.hidden.message', true);
        $messageContainer.removeClass('hidden');
    };

    n.QfqForm.prototype.validationSuccess = function (info) {
        var $formControl = $(info.data.element);
        var $messageContainer = $formControl.siblings('.with-errors');

        if ($messageContainer.length === 0) {
            if ($formControl.parent().hasClass('input-group')) {
                $messageContainer = $formControl.parent().siblings('.hidden.with-errors');
            }
        }

        if ($messageContainer.data('qfq.hidden.message') === true) {
            $messageContainer.addClass('hidden');
        }
    };

    /**
     * @private
     */
    n.QfqForm.prototype.setupEnterKeyHandler = function () {
        $("input").keyup(function (event) {
            if (this.formImmutableDueToConcurrentAccess) {
                return;
            }
            if (event.which === 13 && this.submitOnEnter()) {
                if (this.isFormChanged()) {
                    this.lastButtonPress = "save&close";
                    n.Log.debug("save&close click");
                    this.submit();
                }
                event.preventDefault();
            }
        }.bind(this));
    };


    /**
     *
     * @private
     */
    n.QfqForm.prototype.readFormConfigurationData = function () {
        var $configuredElements = $("#" + this.formId + " [data-hidden],#" + this.formId + " [data-disabled],#" + this.formId + " [data-required]");

        var configurationArray = [];
        $configuredElements.each(function (index, element) {
            try {
                var $element = $(element);
                if (!element.hasAttribute("name")) {
                    n.Log.warning("Element has configuration data, but no name. Skipping");
                    return;
                }

                var configuration = {};
                configuration['form-element'] = $element.attr('name');

                var hiddenVal = $element.data('hidden');
                if (hiddenVal !== undefined) {
                    configuration.hidden = n.Helper.stringToBool(hiddenVal);
                }

                var disabledVal = $element.data('disabled');
                if (disabledVal !== undefined) {
                    configuration.disabled = n.Helper.stringToBool(disabledVal);
                }

                var requiredVal = $element.data("required");
                if (requiredVal !== undefined) {
                    configuration.required = n.Helper.stringToBool(requiredVal);
                }

                configurationArray.push(configuration);
            } catch (e) {
                n.Log.error(e.message);
            }
        });

        return configurationArray;

    };

    /**
     * @public
     * @param bsTabs
     */
    n.QfqForm.prototype.setBsTabs = function (bsTabs) {
        this.bsTabs = bsTabs;
    };

    n.QfqForm.prototype._createError = function (message) {
        var messageButtons = [{
            label: "Ok",
            eventName: 'close'
        }];
        var alert = new n.Alert({ "message": message, "type": "error", modal: true, buttons: messageButtons});
        alert.show();
    };

    /**
     * @private
     */
    n.QfqForm.prototype.fileDeleteSuccessHandler = function (obj) {
        if (!obj.data.status) {
            throw Error("Response on file upload missing status");
        }

        if (obj.data.status === "error") {
            this._createError(obj.data.message);
            return;
        }

        var $button = $(obj.target);
        $button.prop("disabled", true);

        var $buttonParent = $button.parent();
        $buttonParent.addClass('hidden');

        var $inputFile = $buttonParent.siblings('label');
        $inputFile.children(':file').prop("disabled", false);
        $inputFile.removeClass('hidden');
        $inputFile.children(':file').removeClass('hidden');

        $inputFile.children(':file').val("");
        if ($inputFile.children(':file').data('required') == 'required') {
            $inputFile.children(':file').prop("required", true);
        }

        this.form.markChanged();
    };

    /**
     * @private
     */
    n.QfqForm.prototype.fileUploadSuccessHandler = function (obj) {
        if (!obj.data.status) {
            throw Error("Response on file upload missing status");
        }

        if (obj.data.status === "error") {
            //this._createError(obj.data.message);
            var messageButtons = [{
                label: "Ok",
                eventName: 'close'
            }];
            var alert = new n.Alert({ "message": obj.data.message, "type": obj.data.status, modal: true, buttons: messageButtons});
            alert.show();
            return false;
        }

        var $fileInput = $(obj.target);
        $fileInput.prop("disabled", true);
        $fileInput.addClass("hidden");
        $fileInput.parent().addClass("hidden");

        var $deleteContainer = $fileInput.parent().siblings('div.uploaded-file');


        var fileNamesString = obj.target.files[0].name;
        var $fileNameSpan = $deleteContainer.find("span.uploaded-file-name");
        $fileNameSpan.empty().append(fileNamesString);

        var $deleteButton = $deleteContainer.find("button");
        $deleteButton.prop("disabled", false);

        $deleteContainer.removeClass("hidden");
    };

    /**
     *
     * @param $button
     * @param enabled {boolean}
     *
     * @private
     */
    n.QfqForm.prototype.setButtonEnabled = function ($button, enabled) {
        if (!$button) {
            n.Log.error("QfqForm#setButtonEnabled(): no button provided.");
            return;
        }
        if (!enabled) {
            $button.addClass("disabled");
            $button.prop("disabled", true);
        } else {
            $button.removeClass("disabled");
            $button.prop("disabled", false);
        }
    };

    /* Dynamic Update Trigger */
    n.QfqForm.prototype.setupFormUpdateHandler = function () {
        $('textarea[data-load],input[data-load],select[data-load]').on('change', this.formUpdateHandler.bind(this));
    };

    n.QfqForm.prototype.formUpdateHandler = function () {
        var that = this;
        if (this.formImmutableDueToConcurrentAccess) {
            return;
        }
        $.post(this.dataRefreshUrl, this.form.serialize(), "json")
            .fail(n.Helper.showAjaxError)
            .done(function (data) {
                this.handleFormUpdate(data);
            }.bind(that));

    };

    n.QfqForm.prototype.handleFormUpdate = function (data) {
        if (!data.status) {
            throw new Error("Expected 'status' attribute to be present.");
        }

        if (data.status === "error") {
            this._createError("Error while updating form:<br>" +
                             (data.message ? data.message : "No reason given"));
            return;
        }

        if (data.status === "success") {
            if (!data['form-update']) {
                throw new Error("'form-update' attribute missing in form update data");
            }


            this.applyFormConfiguration(data['form-update']);
            this.applyElementConfiguration(data['element-update']);
            return;
        }

        throw new Error("Unexpected status: '" + data.status + "'");
    };

    /**
     * @private
     */
    n.QfqForm.prototype.destroyFormAndSetText = function (text) {
        this.form = null;
        $('#' + this.formId).replaceWith($("<p>").append(text));
        this.eventEmitter.emitEvent('qfqform.destroyed', n.EventEmitter.makePayload(this, null));
    };

    /**
     * @private
     */
    n.QfqForm.prototype.handleSaveClick = function () {
        this.lastButtonPress = "save";
        n.Log.debug("save click");
        this.getSaveButton().removeClass('btn-info');
        this.getSaveButton().addClass('btn-warning active disabled');
        if (!this.form.saveInProgress) {
            this.submit();
        }
    };

    n.QfqForm.prototype.callSave = function(uri) {
        if(this.isFormChanged()) {
            this.handleSaveClick();
            this.goToAfterSave = uri;
        } else {
            window.location = uri;
            return;
        }
    };

    /**
     * @private
     */
    n.QfqForm.prototype.handleCloseClick = function () {
        this.lastButtonPress = "close";
        if (this.form.getFormChanged()) {
            var alert = new n.Alert({
                message: "You have unsaved changes. Do you want to save first?",
                type: "warning",
                modal: true,
                buttons: [
                    {label: "Yes", eventName: "yes"},
                    {label: "No", eventName: "no", focus: true},
                    {label: "Cancel", eventName: "cancel"}
                ]
            });
            var that = this;
            alert.on('alert.yes', function () {
                that.submit();
            });
            alert.on('alert.no', function () {
                that.releaseLock();
                that.eventEmitter.emitEvent('qfqform.close-intentional', n.EventEmitter.makePayload(that, null));

                that.goBack();
            });
            alert.show();
        } else {
            this.goBack();
        }
    };

    n.QfqForm.prototype.submit = function (queryParameters) {
        var submitQueryParameters;
        var alert;
        var submitReason;

        if (this.form.validate() !== true) {


            var element = this.form.getFirstNonValidElement();
            if (element.hasAttribute('name') && this.bsTabs) {
                var tabId = this.bsTabs.getContainingTabIdForFormControl(element.getAttribute('name'));
                if (tabId && this.activateFirstRequiredTab) {
                    this.bsTabs.activateTab(tabId);
                }


                var form = document.getElementById(this.form.formId);
                var inputs = form.elements;
                
                for (var i = 0; i < inputs.length; i++) {
                    var e = inputs[i];
                    if(!e.willValidate) {
                        continue;
                    }
                    if(!e.checkValidity()) {
                        var updateTabId = this.bsTabs.getContainingTabIdForFormControl(e.getAttribute('name'));
                        if(updateTabId != tabId) {
                            this.bsTabs.addDot(updateTabId);
                        }
                    }
                }
            }

            // Since we might have switched the tab, re-validate to highlight errors
            this.form.$form.validator('update');
            this.form.$form.validator('validate');

            this.form.$form.each(function() {
                if (!$(this).validate) {

                }
            });

            if (!this.skipRequiredCheck) {
                alert = new n.Alert("Form is incomplete.", "warning");
                alert.timeout = 3000;
                alert.show();
                return;
            }
        }

        // First, remove all validation states, in case a previous submit has set a validation state, thus we're not
        // stockpiling them.
        if (!this.skipRequiredCheck) {
            this.clearAllValidationStates();
        }

        submitReason = {
            "submit_reason": this.lastButtonPress === "close" ? "save,close" : this.lastButtonPress
        };

        submitQueryParameters = $.extend({}, queryParameters, submitReason);
        this.form.submitTo(this.submitTo, submitQueryParameters);
        this.form.saveInProgress = true;
    };

    /**
     * @private
     */
    n.QfqForm.prototype.handleNewClick = function (event) {
        event.preventDefault();

        this.lastButtonPress = "new";
        if (this.form.getFormChanged()) {
            var alert = new n.Alert({
                message: "You have unsaved changes. Do you want to save first?",
                type: "warning",
                modal: true,
                buttons: [
                    {label: "Yes", eventName: "yes", focus: true},
                    {label: "No", eventName: "no"},
                    {label: "Cancel", eventName: "cancel"}
                ]
            });
            var that = this;
            alert.on('alert.no', function () {
                that.eventEmitter.emitEvent('qfqform.close-intentional', n.EventEmitter.makePayload(that, null));

                var anchorTarget = that.getNewButtonTarget();
                window.location = anchorTarget;
            });
            alert.on('alert.yes', function () {
                that.submit();
            });
            alert.show();
        } else {
            var anchorTarget = this.getNewButtonTarget();
            window.location = anchorTarget;
        }
        n.Log.debug("new click");
    };

    /**
     * @private
     */
    n.QfqForm.prototype.handleDeleteClick = function () {
        this.lastButtonPress = "delete";
        n.Log.debug("delete click");
        var alert = new n.Alert({
            message: "Do you really want to delete the record?",
            type: "warning",
            modal: true,
            buttons: [
                {label: "Yes", eventName: "ok"},
                {label: "No", eventName: "cancel", focus: true}
            ]
        });

        var that = this;
        alert.on('alert.ok', function () {
            $.post(that.appendQueryParametersToUrl(that.deleteUrl, that.getRecordHashMd5AsQueryParameter()))
                .done(that.ajaxDeleteSuccessDispatcher.bind(that))
                .fail(n.Helper.showAjaxError);
        });
        alert.show();
    };

    n.QfqForm.prototype.getRecordHashMd5AsQueryParameter = function () {

        return {
            'recordHashMd5': this.getRecordHashMd5(),
            'tabUniqId': this.getTabUniqId()
        };
    };

    /**
     *
     * @param data
     * @param textStatus
     * @param jqXHR
     *
     * @private
     */
    n.QfqForm.prototype.ajaxDeleteSuccessDispatcher = function (data, textStatus, jqXHR) {
        if (!data.status) {
            throw new Error("No 'status' property 'data'");
        }

        switch (data.status) {
            case "error":
                this.handleLogicDeleteError(data);
                break;
            case "success":
                this.handleDeleteSuccess(data);
                break;
            default:
                throw new Error("Status '" + data.status + "' unknown.");
        }
    };

    /**
     *
     * @param data
     *
     * @private
     */
    n.QfqForm.prototype.handleDeleteSuccess = function (data) {
        this.setButtonEnabled(this.getCloseButton(), false);
        this.setButtonEnabled(this.getDeleteButton(), false);
        this.setButtonEnabled(this.getSaveButton(), false);
        this.setButtonEnabled(this.getNewButton(), false);
        this.destroyFormAndSetText("Record has been deleted!");

        if (!data.redirect || data.redirect === "auto") {
            this.goBack();
            return;
        }

        if (data.redirect === "no") {
            this._createError("redirect=='no' not allowed");
            return;
        }

        if (data.redirect === "url" && data['redirect-url']) {
            window.location = data['redirect-url'];
        }

        if (data.redirect === "url-skip-history" && data['redirect-url']) {
            window.location.replace(data['redirect-url']);
        }
    };

    /**
     *
     * @param data
     *
     * @private
     */
    n.QfqForm.prototype.handleLogicDeleteError = function (data) {
        if (!data.message) {
            throw Error("Status is 'error' but required 'message' attribute is missing.");
        }
        this._createError(data.message);

        this.setButtonEnabled(this.getDeleteButton(), false);
    };

    /**
     * Called when form is changed.
     *
     * @param obj {n.QfqForm}
     *
     * @private
     */
    n.QfqForm.prototype.changeHandler = function (obj) {
        if (this.formImmutableDueToConcurrentAccess) {
            return;
        }
        this.getSaveButton().removeClass("disabled");
        this.getSaveButton().addClass(this.getSaveButtonAttentionClass());
        this.getSaveButton().removeAttr("disabled");
        this.fireDirtyRequestIfRequired();
    };

    n.QfqForm.prototype.fireDirtyRequestIfRequired = function () {
        if (this.dirtyFired) {
            return;
        }

        if (this.lockRenewalPhase) {
            this.dirty.renew(this.getSip(), this.getRecordHashMd5AsQueryParameter());
        } else {
            this.dirty.notify(this.getSip(), this.getRecordHashMd5AsQueryParameter());
        }
        this.dirtyFired = true;
    };

    /**
     *
     * @param obj {n.QfqForm}
     *
     * @private
     */
    n.QfqForm.prototype.resetHandler = function (obj) {
        this.getSaveButton().removeClass(this.getSaveButtonAttentionClass());
        this.getSaveButton().addClass("disabled");
        this.getSaveButton().attr("disabled", "disabled");
        this.resetLockState();
    };

    n.QfqForm.prototype.deactivateSaveButton = function () {
        this.getSaveButton().addClass("disabled");
        //this.getSaveButton().attr("disabled", "disabled");
        this.getSaveButton().off('click');
        this.getSaveButton().on('click', this.infoLockedButton);
        this.getSaveButton().css("color", "#fff");
    };

    n.QfqForm.prototype.infoLockedButton = function(e) {
        var alert = new n.Alert({
            message: "Please wait until the upload finishes to save this form",
            buttons: [{ label: "Ok", eventName: "ok"}],
            modal: true
        });
        alert.show();
        e.preventDefault();
        return false;
    };

    n.QfqForm.prototype.activateSaveButton = function () {
        this.getSaveButton().off('click');
        this.getSaveButton().removeClass("disabled");
        //this.getSaveButton().removeAttr("disabled");
        this.getSaveButton().css("color", "");
        this.getSaveButton().click(this.handleSaveClick.bind(this));
    };

    n.QfqForm.prototype.getSaveButtonAttentionClass = function () {
        var $saveButton = this.getSaveButton();

        return $saveButton.data('class-on-change') || 'alert-warning';
    };

    /**
     *
     * @returns {jQuery|HTMLElement}
     *
     * @private
     */
    n.QfqForm.prototype.getSaveButton = function () {
        return $("#save-button");
    };

    /**
     *
     * @returns {jQuery|HTMLElement}
     *
     * @private
     */
    n.QfqForm.prototype.getCloseButton = function () {
        return $("#close-button");
    };

    /**
     *
     * @returns {jQuery|HTMLElement}
     *
     * @private
     */
    n.QfqForm.prototype.getDeleteButton = function () {
        return $("#delete-button");
    };

    /**
     *
     * @returns {jQuery|HTMLElement}
     *
     * @private
     */
    n.QfqForm.prototype.getNewButton = function () {
        return $("#form-new-button");
    };



    /**
     * @private
     */
    n.QfqForm.prototype.submitSuccessDispatcher = function (obj) {
        if (!obj.data.status) {
            throw new Error("No 'status' property in 'data'");
        }

        switch (obj.data.status) {
            case "error":
                this.handleLogicSubmitError(obj.target, obj.data);
                break;
            case "success":
                this.handleSubmitSuccess(obj.target, obj.data);
                break;
            case "conflict":
                this.handleConflict(obj.target, obj.data);
                break;
            case "conflict_allow_force":
                this.handleOverrideableConflict(obj.target, obj.data);
                break;
            default:
                throw new Error("Status '" + obj.data.status + "' unknown.");
        }

    };

    /**
     *
     * @param form
     * @param data
     *
     * @private
     */
    n.QfqForm.prototype.handleLogicSubmitError = function (form, data) {
        if (!data.message) {
            throw Error("Status is 'error' but required 'message' attribute is missing.");
        }
        this._createError(data.message);

        if (data["field-name"] && this.bsTabs) {
            var tabId = this.bsTabs.getContainingTabIdForFormControl(data["field-name"]);
            if (tabId) {
                this.bsTabs.activateTab(tabId);
            }

            this.setValidationState(data["field-name"], "error");
            this.setHelpBlockValidationMessage(data["field-name"], data["field-message"]);
        }
    };

    /**
     *
     */
    n.QfqForm.prototype.handleConflict = function (form, data) {
        this.setButtonEnabled(this.getSaveButton(), false);
        this.getSaveButton().removeClass(this.getSaveButtonAttentionClass());
        this.setButtonEnabled(this.getDeleteButton(), false);
        this.formImmutableDueToConcurrentAccess = true;
        this.lockAcquired = false;
        this._createError(data.message);
    };

    n.QfqForm.prototype.handleOverrideableConflict = function (form, data) {
        var that = this;
        var alert = new n.Alert({
            message: data.message + 'Save anyway?',
            type: "warning",
            modal: true,
            buttons: [
                {label: "Yes", eventName: "yes"},
                {label: "No", eventName: "no", focus: true}
            ]
        });
        alert.on('alert.yes', function () {
            if (data.tokenForce) {
                that.submit({
                    tokenForce: data.tokenForce
                });
            } else {
                that.submit();
            }

        });
        alert.show();
    };

    /**
     *
     * @param form
     * @param data
     *
     * @private
     */
    n.QfqForm.prototype.handleSubmitSuccess = function (form, data) {
        n.Log.debug('Reset form state');
        form.resetFormChanged();
        this.resetLockState();

        switch (this.lastButtonPress) {
            case 'save&close':
                this.goBack();
                break;
            case 'save':
                if (data.message) {
                    var alert = new n.Alert(data.message);
                    alert.timeout = 3000;
                    alert.show();
                }

                // Skip other checks if external Save is called
                if (this.goToAfterSave) {
                    console.log("Called goToAfterSave = " + this.goToAfterSave);
                    window.location = this.goToAfterSave;
                    return;
                }

                // do we have to update the HTML Form?
                if (data['form-update']) {
                    this.applyFormConfiguration(data['form-update']);
                }

                if (data['element-update']) {this.applyElementConfiguration(data['element-update']);
                }

                if (data.redirect === "url" && data['redirect-url']) {
                    window.location = data['redirect-url'];
                    return;
                }

                if (data.redirect === "url-skip-history" && data['redirect-url']) {
                    window.location.replace(data['redirect-url']);
                    return;
                }

                if (data.redirect === "close") {
                    this.goBack();
                    return;
                }

                break;
            case 'close':
                if (!data.redirect || data.redirect === "no") {
                    return;
                }

                if (data.redirect === "auto" || data.redirect === "close") {
                    this.goBack();
                    return;
                }

                if (data.redirect === "url" && data['redirect-url']) {
                    window.location = data['redirect-url'];
                    return;
                }

                if (data.redirect === "url-skip-history" && data['redirect-url']) {
                    window.location.replace(data['redirect-url']);
                    return;
                }

                break;
            case 'new':
                var target = this.getNewButtonTarget();

                window.location.replace(target);
                return;

            default:
                if (data.redirect === "auto") {
                    this.goBack();
                    return;
                }

                if (data.redirect === "url" && data['redirect-url']) {
                    window.location = data['redirect-url'];
                    return;
                }

                if (data.redirect === "url-skip-history" && data['redirect-url']) {
                    window.location.replace(data['redirect-url']);
                    return;
                }

                break;
        }

        if(this.skipRequiredCheck) {
            this.form.$form.validator('update');
            this.form.$form.validator('validate');
        }
    };

    n.QfqForm.prototype.getNewButtonTarget = function () {
        return $('#form-new-button').attr('href');
    };

    n.QfqForm.prototype.getFormGroupByControlName = function (formControlName) {
        console.log("Form Control Name: " + formControlName);
        var $formControl = $("[name='" + formControlName + "']");
        if ($formControl.length === 0) {
            n.Log.debug("QfqForm.setValidationState(): unable to find form control with name '" + formControlName + "'");
            return null;
        }

        var iterator = $formControl[0];
        while (iterator !== null) {
            var $iterator = $(iterator);
            if ($iterator.hasClass('form-group')) {
                return $iterator;
            }

            iterator = iterator.parentElement;
        }

        return null;
    };

    n.QfqForm.prototype.setValidationState = function (formControlName, state) {
        var $formGroup = this.getFormGroupByControlName(formControlName);
        if ($formGroup) {
            $formGroup.addClass("has-" + state);
            $formGroup.addClass("testitest");
        }
    };

    n.QfqForm.prototype.resetValidationState = function (formControlName) {
        var $formGroup = this.getFormGroupByControlName(formControlName).find('input');
        $formGroup.removeClass("has-danger");
        $formGroup.removeClass("has-error");
        $formGroup.removeClass("has-success");
        $formGroup.removeClass("has-danger");
    };


    n.QfqForm.prototype.clearAllValidationStates = function () {
        // Reset any messages/states added by bootstrap-validator.
        this.form.$form.validator('reset');

        // Reset any states added by a call to QfqForm#setValidationState()
        $('.has-warning,.has-error,.has-success,.has-danger').removeClass("has-warning has-error has-success" +
            " has-danger");

        // Remove all messages received from server upon form submit.
        $('[data-qfq=validation-message]').remove();
    };

    /**
     *
     * @param formControlName
     * @param text
     */
    n.QfqForm.prototype.setHelpBlockValidationMessage = function (formControlName, text) {
        /*
         * Why is this method here and not in FormGroup? Adding this particular method to FormGroup is easy, however
         * QfqForm.clearAllValidationStates() would not find its proper place in FormGroup, since FormGroup operates
         * on one element. We would end up having the responsibilities spread across several classes, which would be
         * confusing.
         */
        var $formGroup = this.getFormGroupByControlName(formControlName);
        if (!$formGroup) {
            return;
        }
        var $helpBlockColumn;
        var $formGroupSubDivs = $formGroup.find("div");
        if ($formGroupSubDivs.length < 3) {
            $helpBlockColumn = $("<div>").addClass("col-md-4");
            $formGroup.append($helpBlockColumn);
        } else {
            $helpBlockColumn = $($formGroupSubDivs[2]);
        }

        $helpBlockColumn.append(
            $("<p>")
                .addClass("help-block")
                .attr("data-qfq", "validation-message")
                .append(text)
                .prepend($("<div>", { class: "arrow arrow-up"}))
        );
    };

    /**
     *
     * @param configuration {array} array of objects.
     */
    n.QfqForm.prototype.applyFormConfiguration = function (configuration) {
        var arrayLength = configuration.length;
        for (var i = 0; i < arrayLength; i++) {
            var configurationItem = configuration[i];
            var formElementName = configurationItem["form-element"];
            if (formElementName === undefined) {
                n.Log.error("configuration lacks 'form-element' attribute. Skipping.");
                continue;
            }
            try {
                var element = n.Element.getElement(formElementName);

                // Cleaner way to set states for tinymce
                // This triggers the event on the unaccesable textarea
                // The tinymce registers a listener on the textarea
                // See helper/tinyMCE.js for details
                if(element.$element.hasClass('qfq-tinymce')) {
                    element.$element.trigger("blur", [configurationItem]);
                }

                if (configurationItem.value !== undefined) {
                    element.setValue(configurationItem.value);
                }

                if (configurationItem.readonly !== undefined) {
                    // Readonly and disabled is the same in our domain
                    element.setEnabled(!configurationItem.readonly);
                }

                if (configurationItem.disabled !== undefined) {
                    // Readonly and disabled is the same in our domain
                    element.setEnabled(!configurationItem.disabled);
                }

                if (configurationItem.hidden !== undefined) {
                    element.setHidden(configurationItem.hidden);
                }

                if (configurationItem.required !== undefined) {
                    element.setRequired(configuration.required);
                    if(element.$element) {
                        if(element.$element.is("select")) {
                            element.$element.prop('required', configurationItem.required);
                            element.$element.attr('data-required', 'yes');
                        }
                        if(element.$element.is("input[type=hidden]")) {
                            console.log("Update Hidden");
                            element.$element.prop("required", configurationItem.required);
                        }

                    }
                }
            } catch (e) {
                n.Log.error(e.message);
            }
        }
    };

    n.QfqForm.prototype.applyElementConfiguration = function (configuration) {
        if (!configuration) {
            console.error("No configuration for Element Update found");
            return;
        }

        n.ElementUpdate.updateAll(configuration);
    };

    /**
     * @private
     * @param obj
     */
    n.QfqForm.prototype.startUploadHandler = function (obj) {
        $(obj.target).after(
            $('<i>').addClass('spinner')
        );
        this.deactivateSaveButton();
    };

    /**
     * @private
     * @param obj
     */
    n.QfqForm.prototype.endUploadHandler = function (obj) {
        var $siblings = $(obj.target).siblings();
        $siblings.filter("i").remove();
        this.activateSaveButton();
    };

    /**
     * Retrieve SIP as stored in hidden input field.
     *
     * @returns {string} sip
     */
    n.QfqForm.prototype.getSip = function () {
        return this.getValueOfHiddenInputField('s');
    };

    /**
     * Retrieve recordHashMd5 as stored in hidden input field.
     *
     * @returns {string} sip
     */
    n.QfqForm.prototype.getRecordHashMd5 = function () {
        return this.getValueOfHiddenInputField('recordHashMd5');
    };

    /**
     * Misuse the window.name attribute to set/get a tab uniq identifier.
     * Use millisecond timestamp as identifier: hopefully there are never more than one tab opened per millisecond in a single browser session.
     *
     * @returns {string} tab identifier
     */
    n.QfqForm.prototype.getTabUniqId = function () {

        if (!window.name.toString()) {
            // Misuse window.name as tab uniq identifier. Set window.name if it is empty.
            window.name = Date.now().toString();
        }

        return window.name;
    };

    n.QfqForm.prototype.getValueOfHiddenInputField = function (fieldName) {
        return $('#' + this.formId + ' input[name=' + fieldName + ']').val();
    };

    /**
     * @public
     */
    n.QfqForm.prototype.isFormChanged = function () {
        return this.form.formChanged;
    };

    /**
     * @private
     */
    n.QfqForm.prototype.submitOnEnter = function () {
        return !(!!this.form.$form.data('disable-return-key-submit'));
    };

    n.QfqForm.prototype.appendQueryParametersToUrl = function (url, queryParameterObject) {
        var queryParameterString = $.param(queryParameterObject);
        if (url.indexOf('?') !== -1) {
            return url + "&" + queryParameterString;
        }

        return url + "?" + queryParameterString;
    };

    /**
     * @private
     *
     * Go back in the history, or pop an alert when no history.
     */
    n.QfqForm.prototype.goBack = function () {
        var alert;

        if (window.history.length < 2) {
            alert = new n.Alert(
                {
                    type: "info",
                    message: "Please close the tab/window."
                }
            );

            alert.show();
            return;
        }

        window.history.back();
    };

})(QfqNS);