// --
// Copyright (C) 2001-2018 OTRS AG, http://otrs.com/
// --
// This software comes with ABSOLUTELY NO WARRANTY. For details, see
// the enclosed file COPYING for license information (AGPL). If you
// did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
// --

/*eslint-disable no-window*/

"use strict";

var Core = Core || {};

/**
 * @namespace Core.SystemConfiguration
 * @memberof Core
 * @author OTRS AG
 * @description
 *      This namespace contains the special functions for SystemConfiguration module.
 */
 Core.SystemConfiguration = (function (TargetNS) {

    var ValueTypes = new Array();

    /**
     * @private
     * @name SerializeData
     * @memberof Core.SystemConfiguration
     * @function
     * @returns {String} query string of the data
     * @param {Object} Data - The data that should be converted.
     * @description
     *      Converts a given hash into a query string.
     */
    function SerializeData(Data) {
        var QueryString = '';
        $.each(Data, function (Key, Value) {
            QueryString += ';' + encodeURIComponent(Key) + '=' + encodeURIComponent(Value);
        });
        return QueryString;
    }

    /**
    * @name Init
    * @memberof Core.SystemConfiguration
    * @function
    * @description
    *      This function initializes module functionality.
    */
    TargetNS.Init = function () {
        var Index,
            Type;

        $("button.RemoveButton").on("click", function() {
            $(this).blur();
            TargetNS.RemoveItem($(this));
        });

        $("button.AddArrayItem").on("click", function() {
            $(this).blur();
            TargetNS.AddArrayItem($(this));
        });
        $("button.AddHashKey").on("click", function() {
            $(this).blur();
            TargetNS.AddHashKeyClick($(this));
        });

        $("a.ResetSetting").on("click", function() {
            Core.Agent.Admin.SystemConfiguration.InitDialogReset($(this));
            return false;
        });

        $("a.RevertToHistoricalValue").on("click", function() {
            TargetNS.RevertToHistoricalValue($(this));
            return false;
        });

        $("input:checkbox").on("click", function() {
            TargetNS.CheckboxValueSet($(this));
        });

        // Init short cuts
        $('.WidgetSimple.Setting').on('keydown', 'input, select, checkbox', function(Event) {

            var $WidgetObj = $(this).closest('.WidgetSimple.Setting');

            if (!Event) {
                return false;
            }

            if ($WidgetObj.hasClass('IsLockedByMe')) {

                // Enter: Save current setting
                if (Event.keyCode && Event.keyCode === 13) {

                    if ($(this).hasClass('Key')) {
                        $(this).closest('.HashItem').find('button.AddKey').trigger('click');
                    }
                    else {
                        $WidgetObj.find('button.Update').trigger('click');
                    }

                    return false;
                }

                // Esc: Cancel editing
                else if (Event.keyCode && Event.keyCode === 27) {
                    $WidgetObj.find('button.Cancel').trigger('click');
                }
            }
        });

        TargetNS.InitButtonVisibility();


        // init ValueTypes
        for (Index in window["Core"]["SystemConfiguration"]) {
            Type = typeof window["Core"]["SystemConfiguration"][Index];
            if (Type != "function") {
                ValueTypes.push(Index);
            }
        }

        Core.App.Subscribe('SystemConfiguration.SettingListUpdate', function() {

            // check if we need to disable editAll/SaveAll
            if (!$('.WidgetSimple.Setting:visible').length) {
                $('#EditAll, #SaveAll, #CancelAll').prop('disabled', true).addClass('Disabled');
            }
            else {
                $('#EditAll, #SaveAll, #CancelAll').prop('disabled', false).removeClass('Disabled');
            }

            if (!$('.WidgetSimple.Setting.IsLockedByMe:visible').length) {
                $('#SaveAll, #CancelAll').prop('disabled', true).addClass('Disabled');
            }
            else {
                $('#SaveAll, #CancelAll').prop('disabled', false).removeClass('Disabled');
            }
        });

        Core.UI.Table.InitTableFilter($("#FilterUsers"), $("#Users"));
    };


    /**
     * @public
     * @name InitConfigurationTree
     * @memberof Core.SystemConfiguration
     * @function
     * @param {String} RedirectAction - Action parameter for next http request.
     * @param {String} Category - If a category should be considered when the tree is rendered (category key)
     * @param {String} UserModificationActive - Limit to settings that can be modified by users.
     * @returns {Boolean} - redirect
     * @description
     *      This function initializes Configuration Tree
     */
    TargetNS.InitConfigurationTree = function(RedirectAction, Category, UserModificationActive) {

        var Data = {
            Action    : 'AdminSystemConfiguration',
            Subaction : 'AJAXNavigationTree',
            Category  : Category || $('#Category').val(),
            UserModificationActive : UserModificationActive
        };

        if (Core.Config.Get('Action') == 'AgentPreferences') {
            Data.Action = 'AgentPreferences';
        }

        // if there are only the base categories available, hide the selection
        // and use 'All' as default.
        if ($('#Category option').length <= 2) {
            $('.SystemConfigurationCategories').hide();
            $('.SystemConfigurationCategories').next('.CallForAction').hide();
            $('#Category').val('All').trigger('change');
            Data["Category"] = 'All';
        }

        $('#ConfigTree').on('click', '.OpenNodeInNewWindow', function(Event) {
            window.open(Core.Config.Get('CGIHandle') + '?Action=AdminSystemConfigurationGroup;RootNavigation=' + $(this).data('node'), '_blank');
            Event.preventDefault;
            return false;
        });

        if (Core.Config.Get('Action') == 'AgentPreferences') {
            Data["IsValid"] = 1;
        }

        if (!$('#ConfigTree').length) {
            return false;
        }

        Core.AJAX.FunctionCall(
            Core.Config.Get('CGIHandle'),
            Data,
            function (HTML) {

                var Group,
                    SelectedNode = $('#SelectedNode').val(),
                    Data = {
                        'Action' : RedirectAction
                    };

                if ($('#ConfigTree').hasClass('jstree')) {
                    $('#ConfigTree').jstree('destroy');
                    $('#ConfigTree').empty();
                }

                $('#ConfigTree').html(HTML);

                $('#ConfigTree')
                    .jstree({
                        core: {
                            multiple: false,
                            animation: 70,
                            themes: {
                                name: 'InputField',
                                variant: 'Tree',
                                icons: false,
                                dots: false,
                                url: false
                            }
                        },
                        search: {
                            show_only_matches: true,
                            show_only_matches_children: true
                        },
                        plugins: [ 'search', 'wholerow' ]
                    })
                    .on('after_open.jstree', function (Node, Selected, Event) {  //eslint-disable-line no-unused-vars
                        Core.UI.InitStickyElement();
                    })
                    .on('after_close.jstree', function (Node, Selected, Event) {  //eslint-disable-line no-unused-vars
                        Core.UI.InitStickyElement();
                    })
                    .on('hover_node.jstree', function (Node, Selected, Event) {  //eslint-disable-line no-unused-vars
                        $('#ConfigTree #' + Core.App.EscapeSelector(Selected.node.id)).children('a').append('<span class="OpenNodeInNewWindow" title="' + Core.Language.Translate('Open this node in a new window') + '" data-node="' + Selected.node.id + '"><i class="fa fa-external-link"></i></span>').find('.OpenNodeInNewWindow').fadeIn();
                    })
                    .on('dehover_node.jstree', function (Node, Selected, Event) {  //eslint-disable-line no-unused-vars
                        $('#ConfigTree #' + Core.App.EscapeSelector(Selected.node.id)).find('.OpenNodeInNewWindow').remove();
                    });

                if (SelectedNode) {
                    $('#ConfigTree').jstree('select_node', SelectedNode);
                }

                $('#ConfigTree')
                    .on('select_node.jstree', function (Node, Selected, Event) {  //eslint-disable-line no-unused-vars

                        $('.ContentColumn').html(Core.Template.Render("Agent/WidgetLoading"));

                        Data["RootNavigation"] = Selected.node.id;
                        Group = $("input#Group").val();
                        if (Group != "") {
                            Data["Subaction"] = "Group";
                            Data["Group"] = Group;
                        }
                        Data.Category = $('#Category').val();

                        // check if another user is being edited
                        if ($('#CurrentUserID').val()) {
                            Data.EditUserID = parseInt($('#CurrentUserID').val(), 10);
                        }

                        // Show settings.
                        SettingList(Selected.selected[0]);

                        return false;
                    }
                );

                $("#ConfigTreeSearch").on('keyup keydown', function() {
                    $('#ConfigTree').jstree('search', $(this).val());
                });
            }, 'html'
        );
    };

    /**
     * @public
     * @name Update
     * @memberof Core.SystemConfiguration
     * @function
     * @param {jQueryObject} $Object - jquery object.
     * @param {Boolean} ToggleValid - toggle ValidID
     * @param {Boolean} ToggleUserModification - toggle UserModificationActive
     * @description
     *      This function updates the setting.
     */
    TargetNS.Update = function ($Object, ToggleValid, ToggleUserModification) {
        var
            URL,
            Data = {},
            Action,
            Key,
            Value,
            $Widget = $Object.closest(".WidgetSimple"),
            SettingName = $Widget.find("input[name='SettingName']").val(),
            ValidationError,
            IsValid,
            UserModificationActive,
            Item,
            $InvalidElement;

        // if there are still hash items within the current Widget
        // for which a key field has been added but no value has been
        // added, hinder the user from saving the setting
        if ($Widget.find('button.AddKey:visible').length) {
            alert(Core.Language.Translate('Please add values for all keys before saving the setting.'));
            return;
        }

        CheckIDs($Widget);

        $Widget
            .find('form')
            .find('input:not(:checkbox):not(:file):not([name=SettingName]):not(.Key):not(.InputField_Search),' +
                ' textarea, select, div.Array, div.Hash, .AddArrayItem, .AddHashKey, div.WorkingHoursItem input')
            .filter(':not([disabled=disabled])')
            .each(function () {

            var FullName = $(this).attr('id'),
                Value,
                $Container,
                ValueType;

            if($(this).hasClass("hasDatepicker")) {
                return;
            }

            if ($(this).is('div')) {
                return;
            }

            // run element validation
            if (!Core.Form.Validate.ValidateElement($(this))) {

                // Highlight error
                ValidationError = 1;
                Core.Form.Validate.HighlightError(this, "Error");
                $(this).on("change", function() {
                    Core.Form.Validate.UnHighlightError(this);
                });

                // Continue, check for other errors as well.
                return;
            }

            // check ValueType
            for (Item in ValueTypes) {
                if ($(this).hasClass(ValueTypes[Item])) {
                    ValueType = ValueTypes[Item];
                    break;
                }
            }

            // get value
            if ($(this).hasClass("AddArrayItem")) {
                $Container = $(this).closest(".Array");
                // check if array is empty
                if ($Container.find("> .ArrayItem").length == 0) {
                    if ($Container.hasClass("VacationDaysArray")
                        || $Container.hasClass("VacationDaysOneTimeArray")
                    ) {
                        Value = {};
                    }
                    else {
                        Value = [];
                    }

                    FullName = SettingName + $(this).attr("data-suffix");
                    FullName = FullName.substr(0, FullName.lastIndexOf("_Array"));
                }
                else {
                    // Array is not empty.
                    return;
                }
            }
            else if ($(this).hasClass("AddHashKey")) {
                // check if array is empty
                if ($(this).closest(".Hash").find("> .HashItem > .SettingContent").length == 0) {
                    Value = {};
                    FullName = SettingName + $(this).attr("data-suffix");
                    FullName = FullName.substr(0, FullName.lastIndexOf("_Hash"));
                }
                else {
                    // Hash is not empty.
                    return;
                }
            }
            else if (ValueType != null) {
                // Run dedicated ValueGet() for this ValueType (in a Core.SystemConfiguration.ValueTypeX.js).
                Value = window["Core"]["SystemConfiguration"][ValueType]["ValueGet"]($(this));
                if (Value == null) {
                    return;
                }
            }
            else if ($(this).is('select')) {
                Value = $(this).find('option:selected').val();
            }
            else if (Value === undefined) {
                Value = $(this).val();
            }

            if($(this).parent().parent().hasClass("HashItem")) {
                // update key name
                Key = $(this).parent().parent().find(".Key").val();
                Key = Core.App.EscapeHTML(Key);

                FullName = FullName.substr(0, FullName.lastIndexOf("###"));
                FullName += "###" + Key;
            }

            Data[SettingName] = ValueSet(Data[SettingName], FullName, Value);
        });

        if (ValidationError) {
            $InvalidElement = $Widget.find('.Error:first');

            $('html, body').animate({scrollTop: $InvalidElement.offset().top -100 }, 'slow');

            // Focus first error element.
            $InvalidElement.focus();

            return;
        }

        Value = {
            EffectiveValue: Core.JSON.Stringify(Data[SettingName])
        };

        // show loader
        Core.UI.WidgetOverlayShow($Widget, 'Loading');

        if (Core.Config.Get('Action') == 'AgentPreferences') {
            Action = 'AgentPreferences';
        }
        else {
            Action = 'AdminSystemConfigurationGroup';
        }

        URL = Core.Config.Get('Baselink') +
            'Action=' + Action +
            ';Subaction=SettingUpdate;' +
            'SettingName=' + encodeURIComponent(SettingName) +
            ';ChallengeToken=' + Core.Config.Get('ChallengeToken');

        if (ToggleValid) {
            IsValid = 0;
            if ($Widget.find(".SettingDisabled:visible").length || $Widget.find(".SettingEnable:visible").length) {
                IsValid = 1;
            }

            URL += ';IsValid=' + IsValid;
        }

        if (ToggleUserModification) {
            UserModificationActive = 0;
            if ($Widget.find(".UserModificationNotActive:visible").length) {
                UserModificationActive = 1;
            }

            URL += ';UserModificationActive=' + UserModificationActive;
        }

        Core.AJAX.FunctionCall(
            URL,
            Value,
            function(Response) {

                TargetNS.CleanWidgetClasses($Widget);
                TargetNS.SettingRender(Response, $Widget);

                if (Response.Data.SettingData.IsDirty) {
                    Core.UI.ShowNotification(
                        Core.Language.Translate('You have undeployed settings, would you like to deploy them?'),
                        'Notice',
                        'Action=AdminSystemConfigurationDeployment;Subaction=Deployment',
                        function() {
                            Core.UI.InitStickyElement();
                        }
                    );
                }
                else if (Response.Data.DeploymentNeeded == 0) {
                    Core.UI.HideNotification(
                        Core.Language.Translate('You have undeployed settings, would you like to deploy them?'),
                        'Notice',
                        function() {
                            Core.UI.InitStickyElement();
                        }
                    );
                }

                Core.UI.WidgetOverlayHide($Widget, Response.Data.Error === undefined);
            }
        );
    }

    /**
     * @private
     * @name ValueSet
     * @memberof Core.SystemConfiguration
     * @function
     * @param {Object} Data - existing resulting object
     * @param {String} Name - setting name.
     * @param {Object} Value - subvalue that will be merged to the Data.
     * @returns {Object} - updated Data.
     * @description
     *      This function merges complex Value to the already existing Data
     *      (used for complex structures like AoH, etc)
     */
    function ValueSet(Data, Name, Value) {
        var Result = Data,
            StructureArray = Name.split(/_(?=Array|Hash)/),
            HashKey,
            SettingName = StructureArray.shift(),
            Structure,
            ArrayIndex,
            SubName,
            RemoveIndexes = [],
            Item;

        if (StructureArray.length) {
            Structure = StructureArray.shift();

            if (Structure.match(/Array\d+/)) {
                // contains array, check index

                ArrayIndex = Structure.match(/\d+/)[0];
                ArrayIndex--;

                if (Result === undefined) {
                    Result = [];
                }

                while(Result.length < ArrayIndex) {
                    RemoveIndexes.push(Result.length);  // mark index for removal
                    Result.push("");                    // temporary push empty value, we will remove it later
                }

                if (StructureArray.length == 0) {
                    // last item in the Structure

                    Result.push(Value);
                }
                else {
                    // start recursion
                    SubName =  StructureArray.join("_");
                    if (SubName != "") {
                        SubName = "_" + SubName;
                    }
                    SubName = SettingName + SubName;

                    if (Result.length < ArrayIndex) {
                        Result.push(ValueSet(undefined, SubName, Value));
                    }
                    else {
                        Result[ArrayIndex] = ValueSet(Result[ArrayIndex], SubName, Value);
                    }
                }

                // delete array elements that are not used
                if (RemoveIndexes.length) {
                    RemoveIndexes.reverse();

                    for(ArrayIndex in RemoveIndexes) {
                        Result.splice(RemoveIndexes[ArrayIndex], 1);
                    }
                }

            }
            else if (Structure.match(/Hash/)) {
                // contains hash

                HashKey = Structure.match(/###(.*?)$/)[1];
                if (Result === undefined) {
                    Result = {};
                }

                if (StructureArray.length == 0) {
                    // last item in the Structure

                    Result[HashKey] = Value;
                }
                else {
                    // start recursion
                    SubName =  StructureArray.join("_");
                    if (SubName != "") {
                        SubName = "_" + SubName;
                    }
                    SubName = SettingName + SubName;

                    if (Result[HashKey] === undefined) {
                        Result[HashKey] = ValueSet(undefined, SubName, Value);
                    }
                    else {
                        Result[HashKey] = ValueSet(Result[HashKey], SubName, Value);
                    }
                }
            }
            else {
                // scalar value
                Result = Value;
            }
        }
        else {
            if ($.isArray(Value)) {
                // Value is array
                if (Result === undefined) {
                    Result = [];
                }

                for (Item in Value) {
                    Result.push(ValueSet(Result[Item], "", Value[Item]));
                }

            }
            else if (typeof Value == "object") {
                // Value is object
                if (Result === undefined) {
                    Result = {};
                }

                for (Item in Value) {
                    Result[Item] = ValueSet(Result[Item], "", Value[Item]);
                }
            }
            else {
                // scalar
                Result = Value;
            }
        }

        return Result;
    }

    /**
     * @public
     * @name RemoveItem
     * @memberof Core.SystemConfiguration
     * @function
     * @param {jQueryObject} $Object - jquery object.
     * @description
     *      Remove complete item from the array/hash
     */
    TargetNS.RemoveItem = function ($Object) {
        var $Div = $Object.closest(".Array, .Hash");

        $Object.closest("div")
            .remove();

        CheckMinItems($Div);
        CheckMaxItems($Div);
    }

    /**
     * @public
     * @name AddArrayItem
     * @memberof Core.SystemConfiguration
     * @function
     * @param {jQueryObject} $Object - jquery object.
     * @description
     *      Add new item to the array
     */
    TargetNS.AddArrayItem = function ($Object) {
        var
            Data = 'Action=AdminSystemConfigurationGroup;Subaction=AddArrayItem;',
            $Widget = $Object.closest(".WidgetSimple"),
            $SettingName = $Widget.find("input[name='SettingName']"),
            IDSuffix = $Object.attr("data-suffix"),
            Structure = StructureGet($Object);

        // return if another AJAX call is already started
        if ($Widget.find(".AJAXLoader:visible").length > 0) {
            return;
        }

        Data += 'SettingName=' + encodeURIComponent($SettingName.val()) + ';';
        Data += 'IDSuffix=' + encodeURIComponent(IDSuffix) + ';';
        Data += 'Structure=' + encodeURIComponent(Structure) + ';';

        // show loader
        Core.UI.WidgetOverlayShow($Widget, 'Loading');

        Core.AJAX.FunctionCall(
            Core.Config.Get('Baselink'),
            Data,
            function(Response) {
                var Suffix = $Object.attr("data-suffix"),
                    SuffixID = Suffix.match(/\d+$/)[0],
                    $Array = $Object.parent(),
                    RemoveButton,
                    Key = "",
                    ElementSuffix;

                if (Response.Error) {
                    alert(Response.Error);
                    $Widget.find(".AJAXLoader").hide();
                    return;
                }

                if (Response.Item) {
                    $Object.before(
                        Core.Template.Render("SysConfig/AddArrayItem")
                    );
                    $Array.find(".ArrayItem:last").append(Response.Item);

                    // Add remove button(s)
                    RemoveButton = Core.Template.Render("SysConfig/RemoveButton");

                    $Array.find("> .ArrayItem:last, > .ArrayItem:last .ArrayItem")
                        .append(RemoveButton);

                    $Array.find("> .ArrayItem:last .RemoveButton")
                        .show()
                        .on("click",function() {
                            TargetNS.RemoveItem($(this).closest("div"));
                    });

                    // add buttons
                    $Array.find("> .ArrayItem:last .Array,> .ArrayItem:last .Hash").each(function() {
                        var Count = 0,
                            Suffix = [],
                            $Element = $(this),
                            $HashItem = $(this).closest(".HashItem");

                        if ($HashItem.length) {
                            Key = $HashItem.find(".Key").val();
                        }

                        while (!$Array.is($Element)) {

                            Count = $Element.find(
                                "> .Array > .ArrayItem, > .Hash > .HashItem, > .ArrayItem, > .HashItem"
                            ).length;
                            Count++;

                            if ($Element.hasClass("Array")) {
                                // array
                                Suffix.push("_Array" + Count);
                            }
                            else {
                                // hash
                                Suffix.push("_Hash###" + Key);
                            }

                            $Element = $Element.parent().closest(".Hash, .Array");
                        }
                        // Remove first item, since it is already included in the IDSuffix
                        Suffix.shift();

                        ElementSuffix = IDSuffix + Suffix.join("");

                        $(this).find("> .ArrayItem:last").after(
                            Core.Template.Render("SysConfig/AddButton", {
                                Suffix : ElementSuffix + "_Array" + Count,
                                Class: 'AddArrayItem'
                            })
                        );

                        $(this).find("> .HashItem:last").after(
                            Core.Template.Render("SysConfig/AddButton", {
                                Suffix : ElementSuffix + "_Hash###",
                                Class: 'AddHashKey'
                            })
                        );

                        $(this).find("> button.AddArrayItem")
                            .show()
                            .off("click")
                            .on("click", function() {
                                TargetNS.AddArrayItem($(this));
                        });

                        $(this).find("button.AddKey")
                            .show()
                            .off("click")
                            .on("click", function() {
                                AddHashKey($(this));
                        });
                        $(this).find("> button.AddHashKey")
                            .show()
                            .off("click")
                            .on("click", function() {
                                TargetNS.AddHashKeyClick($(this));
                        });
                    });

                    Core.UI.InputFields.InitSelect(
                        $Array.find("> .ArrayItem:last .Modernize")
                    );

                    $Array.find("> .ArrayItem:last input:checkbox").on("change", function () {
                        TargetNS.CheckboxValueSet($(this));
                    });
                }

                CheckMinItems($Object);
                CheckMaxItems($Object);


                // increment and replace suffix id
                SuffixID++;
                Suffix = Suffix.replace(/\d+$/, SuffixID);
                $Object.attr("data-suffix", Suffix);

                Core.UI.WidgetOverlayHide($Widget);
            }
        );
    }

    /**
     * @public
     * @name AddHashKeyClick
     * @memberof Core.SystemConfiguration
     * @function
     * @param {jQueryObject} $Object - jquery object.
     * @description
     *      Handle add new hash key
     */
    TargetNS.AddHashKeyClick = function ($Object) {
        var $Widget = $Object.closest(".WidgetSimple"),
            RemoveButton = Core.Template.Render("SysConfig/RemoveButton");

        // return if another item is currently being added
        if ($Widget.find(".AJAXLoader:visible").length > 0) {
            return;
        }

        Core.UI.WidgetOverlayShow($Widget, 'Loading');

        $Object.before(Core.Template.Render("SysConfig/AddHashKey", {
            'IDSuffix' : $Object.attr("data-suffix")
        }));

        $Object
            .closest("div")
            .find("button.AddKey:last")
            .off("click")
            .on("click", function() {
                AddHashKey($(this));
            })
            .after(RemoveButton);

        $Object
            .closest("div")
            .find("button.RemoveButton:last").on("click",function() {
                TargetNS.RemoveItem($(this).closest("div"));
            });

        CheckMinItems($Object);
        CheckMaxItems($Object);

        Core.UI.WidgetOverlayHide($Widget);
    }

    /**
     * @private
     * @name AddHashKey
     * @memberof Core.SystemConfiguration
     * @function
     * @param {jQueryObject} $Object - jquery object.
     * @description
     *      Add new hash key
     */
    function AddHashKey($Object) {
        /* Needs to be an AJAX call */

        var
            $Widget = $Object.closest(".WidgetSimple"),
            $HashItem = $Object.parent(),
            $KeyElement = $HashItem.find(".Key"),
            Key = $KeyElement.val(),
            Name = $Widget.find("input[name='SettingName']").val(),
            Data = 'Action=AdminSystemConfigurationGroup;Subaction=AddHashKey;',
            IDSuffix = $KeyElement.attr("data-suffix") + Key,
            Structure = StructureGet($Object);

        if ($Widget.find(".AJAXLoader:visible").length > 0) {
            return;
        }

        if (!$KeyElement.val()) {
            alert(Core.Language.Translate('The key must not be empty.'));
            return;
        }

        // Check if hash already contains same Key
        if ($HashItem.closest(".Hash").find("> .HashItem >.Key").filter(function() {
                return $(this).val() === Key;
            }).length > 1) {

            alert(Core.Language.Translate("A key with this name ('%s') already exists.", Key));
            return;
        }

        Data += 'Key=' + encodeURIComponent(Key) + ';';
        Data += 'SettingName=' + encodeURIComponent(
            $Widget.find("input[name='SettingName']").val()
        );
        Data += ';IDSuffix=' + encodeURIComponent(IDSuffix) + ';';
        Data += 'Structure=' + encodeURIComponent(Structure) + ';';

        // show loader
        Core.UI.WidgetOverlayShow($Widget, 'Loading');

        Core.AJAX.FunctionCall(
            Core.Config.Get('Baselink'),
            Data,
            function(Response) {
                var $Hash = $Object.closest(".Hash"),
                    RemoveButton,
                    Key = "";

                if (Response.Error) {
                    alert(Response.Error);
                    $Widget.find(".AJAXLoader").hide();
                    return;
                }

                if (Response.Item) {
                    // Set input key name
                    $HashItem.find("input").attr("name", Name + "Key[]");

                    // Remove button
                    $HashItem.find("button.AddKey").remove();

                    // Add new input for hash value
                    $HashItem.append(Response.Item);

                    // Add remove button(s)
                    RemoveButton = Core.Template.Render("SysConfig/RemoveButton");
                    $HashItem.append(RemoveButton);

                    $HashItem.find(".ArrayItem")
                        .append(RemoveButton);

                    $HashItem.find(".RemoveButton")
                        .show()
                        .on("click",function() {
                            TargetNS.RemoveItem($(this).closest("div"));
                    });

                    // add buttons
                    $HashItem.find(".Array, .Hash").each(function() {
                        var Count = 0,
                            Suffix = [],
                            $Element = $(this),
                            $ClosestHashItem = $(this).closest(".HashItem"),
                            ElementSuffix;

                        if ($ClosestHashItem.length) {
                            Key = $ClosestHashItem.find(".Key").val();
                        }

                        while (!$Hash.is($Element)) {

                            Count = $Element.find(
                                "> .Array > .ArrayItem, > .Hash > .HashItem, > .ArrayItem, > .HashItem"
                            ).length;
                            Count++;

                            if ($Element.hasClass("Array")) {
                                // array
                                Suffix.push("_Array" + Count);
                            }
                            else {
                                // hash
                                Suffix.push("_Hash###" + Key);
                            }

                            $Element = $Element.parent().closest(".Hash, .Array");
                        }
                        // Remove first item, since it is already included in the IDSuffix
                        Suffix.shift();

                        ElementSuffix = IDSuffix + Suffix.join("");

                        $(this).find("> .ArrayItem").after(
                            Core.Template.Render("SysConfig/AddButton", {
                                Suffix : ElementSuffix + "_Array" + Count,
                                Class: 'AddArrayItem'
                            })
                        );

                        $(this).find("> .HashItem").after(
                            Core.Template.Render("SysConfig/AddButton", {
                                Suffix : ElementSuffix + "_Hash###",
                                Class: 'AddHashKey'
                            })
                        );

                        $HashItem.find("button.AddArrayItem")
                            .show()
                            .off("click")
                            .on("click", function() {
                                TargetNS.AddArrayItem($(this));
                        });

                        $Hash.find("button.AddKey")
                            .show()
                            .off("click")
                            .on("click", function() {
                                AddHashKey($(this));
                        });

                        $HashItem.find("button.AddHashKey")
                            .show()
                            .off("click")
                            .on("click", function() {
                                TargetNS.AddHashKeyClick($(this));
                        });
                    });

                    $Hash.find("input:checkbox").on("change", function () {
                        TargetNS.CheckboxValueSet($(this));
                    });

                    Core.UI.InputFields.InitSelect(
                        $Hash.find(".Modernize")
                    );

                    $KeyElement.attr("readonly", "readonly");
                }

                Core.UI.WidgetOverlayHide($Widget);
            }
        );
    }

    /**
     * @public
     * @name SettingReset
     * @memberof Core.SystemConfiguration
     * @function
     * @param {jQueryObject} $Object - jquery object
     * @param {Number} ResetOptions - reset-globally,reset-locally
     * @description
     *      Reset setting to the default value.
     */
    TargetNS.SettingReset = function ($Object, ResetOptions) {
        var $Widget = $Object.closest(".WidgetSimple"),
            $SettingName = $Widget.find("input[name='SettingName']"),
            // TODO: Action might need to be changed.
            Data = 'Action=AdminSystemConfigurationGroup;Subaction=SettingReset;';

        Data += 'SettingName=' + encodeURIComponent($SettingName.val()) + ';';
        Data += 'ResetOptions=' + encodeURIComponent(ResetOptions) + ';';

        // show loader
        Core.UI.WidgetOverlayShow($Widget, 'Loading');

        Core.AJAX.FunctionCall(
            Core.Config.Get('Baselink'),
            Data,
            function(Response) {

                TargetNS.SettingRender(Response, $Widget);
                TargetNS.CleanWidgetClasses($Widget);

                if (Response.Data.SettingData.IsDirty) {
                    $Widget.addClass('IsDirty');
                }

                if (Response.Error === undefined) {
                    Core.UI.WidgetOverlayHide($Widget, true);

                    if (Response.DeploymentNeeded == 0) {

                        // hide the "deployment" notification
                        Core.UI.HideNotification(
                            Core.Language.Translate('You have undeployed settings, would you like to deploy them?'),
                            'Notice',
                            function() {
                                Core.UI.InitStickyElement();
                            }
                        );
                    }
                    else {
                        Core.UI.ShowNotification(
                            Core.Language.Translate('You have undeployed settings, would you like to deploy them?'),
                            'Notice',
                            'Action=AdminSystemConfigurationDeployment;Subaction=Deployment',
                            function() {
                                Core.UI.InitStickyElement();
                            }
                        );
                    }
                }
                Core.App.Publish('SystemConfiguration.SettingListUpdate');
            }
        );
    }

    /**
     * @public
     * @name RevertToHistoricalValue
     * @memberof Core.SystemConfiguration
     * @function
     * @param {jQueryObject} $Object - jquery object
     * @returns {Boolean} - false in case delete all values is not needed
     * @description
     *      Delete user setting.
     */
    TargetNS.RevertToHistoricalValue = function ($Object) {
        var $SettingName = $("#SettingName"),
            URL = Core.Config.Get('Baselink') + 'Action=AdminSystemConfigurationSettingHistory;Subaction=RevertToHistoricalValue;';

        URL += 'SettingName=' + encodeURIComponent($SettingName.attr('value')) + ';';
        URL += 'ModifiedVersionID=' + encodeURIComponent($Object.attr('value'));
        URL += SerializeData(Core.App.GetSessionInformation());

        if (window.confirm(Core.Language.Translate("Do you really want to revert this setting to its historical value?"))) {
            window.location = URL;
        }

        return false;
    }


    /**
     * @public
     * @name CheckboxValueSet
     * @memberof Core.SystemConfiguration
     * @function
     * @param {jQueryObject} $Object - jQueryObject
     * @description
     *      This function sets the hidden input field value according to the
     *      checkbox($Object) state.
     */
    TargetNS.CheckboxValueSet = function ($Object) {
        // Set hidden input field value
        $Object
            .next()
            .val($Object.is(":checked") ? 1:0);
    }

    /**
     * @public
     * @name CleanWidgetClasses
     * @memberof Core.Agent.Admin.SystemConfiguration
     * @function
     * @param {jQueryObject} $Widget - The widget jquery object
     * @description
     *      Cleans up all locked/modified classes on the widget
     */
    TargetNS.CleanWidgetClasses = function ($Widget) {
        $Widget.removeClass('IsLockedByMe IsDirty IsModified IsLockedByAnotherUser');
    }

    /**
     * @public
     * @name SettingRender
     * @memberof Core.SystemConfiguration
     * @function
     * @param {Object} Response - AJAX response
     * @param {jQueryObject} $Widget - jQueryObject
     * @description
     *      This function renders setting received by AJAX call(Response) to the provided $Widget.
     */
    TargetNS.SettingRender = function (Response, $Widget) {
        var $EditLink = $Widget.find(".SettingEdit");

        $Widget.find("fieldset").html(Response.Data.HTMLStrg);

        // remove old Icon class
        $EditLink.find("i").removeClass();
        $EditLink.removeClass();

        if (Response.Data.SettingData.ExclusiveLockGUID == "0") {
            // Unlocked

            $EditLink.find("i").addClass("fa fa-pencil-square-o");
            $EditLink.addClass("SettingEdit Unlocked");
            $EditLink.attr("title", Core.Language.Translate("Edit this setting"));
        }
        else {
            // Locked

            $EditLink.find("i").addClass("fa fa-unlock");
            $EditLink.addClass("SettingEdit Locked");
            $EditLink.attr("title", Core.Language.Translate("Unlock setting."));

            $Widget.find("fieldset .Update").on("click", function () {
                TargetNS.Update($(this), 0 , 0);
            });

            $Widget.find("fieldset .Cancel").off("click").on("click", function () {
                Core.Agent.Admin.SystemConfiguration.Cancel($(this));
                return false;
            });

            TargetNS.InitButtonVisibility($Widget);
            $Widget.find("button.AddArrayItem").on("click", function() {
                $(this).blur();
                TargetNS.AddArrayItem($(this));
            });
            $Widget.find("button.AddHashKey").on("click", function() {
                $(this).blur();
                TargetNS.AddHashKeyClick($(this));
            });
            $Widget.find(".RemoveButton").on("click",function() {
                TargetNS.RemoveItem($(this).closest("div"));
            });
        }

        if (Response.Data.SettingData.IsValid == '1') {
            $Widget.find(".Header .Icon i")
                .addClass("Hidden");

            $Widget.find(".SettingDisabled").addClass("Hidden");
            $Widget.find(".SettingEnabled").removeClass("Hidden");
            $Widget.removeClass("IsDisabled");
        }
        else {
            $Widget.find(".Header .Icon i")
                .removeClass("Hidden");

            $Widget.find(".SettingDisabled").removeClass("Hidden");
            $Widget.find(".SettingEnabled").addClass("Hidden");
            $Widget.addClass("IsDisabled");
        }

        if (Response.Data.SettingData.UserModificationActive == '1') {
            $Widget.find(".UserModificationNotActive").addClass("Hidden");
            $Widget.find(".UserModificationActive").removeClass("Hidden");
        }
        else {
            $Widget.find(".UserModificationNotActive").removeClass("Hidden");
            $Widget.find(".UserModificationActive").addClass("Hidden");
        }

        if (Response.Data.Error != null) {
            alert(Response.Data.Error);
        }

        if (Response.Data.HTMLStrg.indexOf("<select") > -1) {
            Core.UI.InputFields.Activate($Widget.find("fieldset"));
        }

        if (Response.Data.SettingData.IsModified) {
            $Widget.addClass('IsModified');
            $Widget.find("button.ResetSetting").show();

            // for user preferences
            $Widget.find(".ResetUserSetting").removeClass("Hidden");
        }
        else {
            $Widget.removeClass('IsModified');

            // for user preferences
            $Widget.find(".ResetUserSetting").addClass("Hidden");
        }

        if (Response.Data.SettingData.IsDirty) {
            $Widget.addClass('IsDirty');
        }

        if ($Widget.hasClass('MenuExpanded')) {
            $Widget.find('.WidgetMessage.Bottom').show();
        }

        $Widget.find("a.ResetSetting")
            .off("click")
            .on("click", function() {

            Core.Agent.Admin.SystemConfiguration.InitDialogReset($(this));

            return false;
        });

        if (Response.Data.SettingData.IsLockedByMe == "1") {
            $Widget.addClass('IsLockedByMe');
        }

        if (Response.Data.SettingData.Invalid != "1") {
            // setting is valid
            $Widget.find(".Icon .fa-check-circle-o").removeClass("Hidden");
        }

        $Widget.find("input:checkbox")
        .off("change")
        .on("change", function() {
            TargetNS.CheckboxValueSet($(this));
        });
    }

    /**
     * @private
     * @name CheckMinItems
     * @memberof Core.SystemConfiguration
     * @function
     * @param {jQueryObject} $Object - jQueryObject
     * @description
     *      Initialize Array/Hash container with MinItems.
     */
    function CheckMinItems($Object) {
        var $Structure = $Object,
            MinItems;

        if (!$Structure.hasClass("Array") && !$Structure.hasClass("Hash")) {
            $Structure = $Object.closest(".Array, .Hash");
        }

        MinItems = $Structure.attr("data-min-items");

        if (MinItems !== undefined) {
            if ($Structure.find("> .ArrayItem, > .HashItem").length <= MinItems) {
                // Hide all Remove buttons
                $Structure.find("> .ArrayItem .RemoveButton, > .HashItem .RemoveButton").hide();
            }
            else {
                // Show all Remove buttons
                $Structure.find("> .ArrayItem .RemoveButton, > .HashItem .RemoveButton").show();
            }
        }
        else {
            // Show all Remove buttons
            $Structure.find("> .ArrayItem .RemoveButton, > .HashItem .RemoveButton").show();
        }
    }

    /**
     * @private
     * @name CheckMaxItems
     * @memberof Core.SystemConfiguration
     * @function
     * @param {jQueryObject} $Object - jQueryObject
     * @description
     *      Initialize Array/Hash container with MaxItems.
     */
    function CheckMaxItems($Object) {
        var $Structure = $Object,
            MaxItems;

        if (!$Structure.hasClass("Array") && !$Structure.hasClass("Hash")) {
            $Structure = $Object.closest(".Array, .Hash");
        }

        MaxItems = $Structure.attr("data-max-items");

        if (MaxItems !== undefined) {
            if ($Structure.find("> .ArrayItem, > .HashItem").length >= MaxItems) {
                // Hide all AddArrayItem and AddHashKey buttons.
                $Structure.find("> .AddArrayItem, > .AddHashKey").hide();
            }
            else {
                // Show all AddArrayItem and AddHashKey buttons.
                $Structure.find("> .AddArrayItem:not(.Hidden), > .AddHashKey:not(.Hidden)").show();
            }
        }
        else {
            // Show all AddArrayItem and AddHashKey buttons.
            $Structure.find("> .AddArrayItem:not(.Hidden), > .AddHashKey:not(.Hidden)").show();
        }
    }

    /**
     * @private
     * @name StructureGet
     * @memberof Core.SystemConfiguration
     * @function
     * @param {jQueryObject} $Object - jQueryObject
     * @param {String} Structure - result of previous call
     * @returns {String} - Structure
     * @description
     *      Get a sub structure of $Object in the complex item.
     */
    function StructureGet($Object, Structure) {
        var Result = "",
            $Item;

        if (Structure) {
            Result = Structure;
        }

        $Item = $Object.parent().closest(".Array, .Hash, .Setting");

        if ($Item.hasClass("Setting")) {
            return Result;
        }
        else if ($Item.hasClass("Array")) {
            if (Result) {
                Result += '.';
            }
            Result += "Array";
        }
        else if ($Item.hasClass("Hash")) {
            if (Result) {
                Result += '.';
            }
            Result += "Hash";
        }

        return StructureGet($Item, Result);
    }

    /**
     * @private
     * @name CheckIDs
     * @memberof Core.SystemConfiguration
     * @function
     * @param {jQueryObject} $Object - jQueryObject
     * @description
     *      Go through all items that are used for EffectiveValue calcutation,
     *      and update their IDs according to the latest changes(array position
     *      or renamed hash key).
     */
    function CheckIDs($Object) {
        $Object.find(".Entry, .AddArrayItem").each(function() {
            var OldID,
                ID,
                Key,
                SubString,
                Count = 0,
                $ClosestItem,
                ValueType,
                Index;

            if ($(this).hasClass("Entry")) {
                OldID = $(this).attr("id");
            }
            else {
                OldID = $(this).attr("data-suffix");
            }
            ID = OldID;

            // check ValueType
            for (Index in ValueTypes) {
                if ($(this).hasClass(ValueTypes[Index])) {
                    ValueType = ValueTypes[Index];
                    break;
                }
            }

            if (ID.indexOf("_Hash###") > 0) {
                // put placeholders
                while (ID.indexOf("_Hash###") > 0) {
                    SubString = ID.match(/(_Hash###.*?)(_Array|_Hash|Day$|Month$|Year$|Hour$|Minute$|$)/)[1];

                    ID = ID.replace(SubString, "_PLACEHOLDER" + Count);
                    Count++;
                }

                $ClosestItem = $(this).closest(".HashItem");

                while (Count > 0) {
                    Count--;

                    // get key value
                    Key = $ClosestItem.find(".Key").val();
                    Key = Core.App.EscapeHTML(Key);

                    // update id
                    ID = ID.replace("_PLACEHOLDER" + Count, "_Hash###" + Key);

                    $ClosestItem = $ClosestItem.parent().closest(".HashItem");
                }

                // set new id
                if ($(this).hasClass("Entry")) {
                    $(this).attr("id", ID);
                }
                else {
                    $(this).attr("data-suffix", ID);
                }
            }
            if (ID.indexOf("_Array") >= 0) {
                $(this).closest('.Array').find(".ArrayItem").each(function() {
                    Count = 0;

                    $(this).find(
                        ".SettingContent input:visible:not(.InputField_Search), " +
                        ".SettingContent select:visible, .SettingContent select.Modernize, " +
                        ".SettingContent textarea:visible"
                    ).each(function() {
                        var OldID = $(this).attr('id');

                        ID = OldID;

                        while (ID.indexOf("_Array") >= 0) {
                            SubString = ID.match(/(_Array\d+)/)[1];
                            ID = ID.replace(SubString, "_PLACEHOLDER" + Count);
                            Count++;
                        }

                        $ClosestItem = $(this).closest('.ArrayItem');

                        while (Count > 0) {
                            Count--;

                            Key = $ClosestItem.index() + 1;
                            ID = ID.replace("_PLACEHOLDER" + Count, "_Array" + Key);
                            // update id
                            $(this).attr('id', ID);

                            $ClosestItem = $ClosestItem.parent().closest(".ArrayItem");
                        }

                        if (OldID != ID && ValueType) {

                            // Run dedicated CheckID() for this ValueType (in a Core.SystemConfiguration.ValueTypeX.js).
                            if (window["Core"]["SystemConfiguration"][ValueType]["CheckID"]) {
                                window["Core"]["SystemConfiguration"][ValueType]["CheckID"]($(this), OldID);
                            }
                        }
                    });
                });
            }
        });
    }

    /**
     * @public
     * @name InitButtonVisibility
     * @memberof Core.SystemConfiguration
     * @function
     * @param {jQueryObject} $Object - jQueryObject
     * @description
     *      Initialize add button visibility.
     */
    TargetNS.InitButtonVisibility = function ($Object) {
        if ($Object === undefined) {
            $Object = $(document);
        }

        /* check hashes and arrays with minimum and maximum items */
        $Object.find(".Hash[data-min-items], .Array[data-min-items]").each(function() {
            CheckMinItems($(this));
        });

        $Object.find(".Hash[data-max-items], .Array[data-max-items]").each(function() {
            CheckMaxItems($(this));
        });

        /* show buttons inside hashes and arrays without minimum/maximum items */
        $Object.find(".Hash:not([data-min-items]), .Array:not([data-min-items])").each(function() {
            $(this).find("> .ArrayItem > .RemoveButton, > .HashItem > .RemoveButton").show();
        });

        $Object.find(".Hash:not([data-max-items]), .Array:not([data-max-items])").each(function() {
            $(this)
                .find("> .AddHashKey:not(.Hidden), > .AddArrayItem:not(.Hidden)").show();
        });
    }

    /**
     * @private
     * @name SettingList
     * @memberof Core.SystemConfiguration
     * @function
     * @param {String} Selected - selected navigation (Frontend::Module)
     * @description
     *      This function reloads list of settings (AJAX) when user clicks on Navigation item.
     */
    function SettingList(Selected) {
        var Data,
            Action = 'AdminSystemConfigurationGroup',
            BreadCrumbItems,
            ListItem,
            URL = Core.Config.Get('CGIHandle'),
            Index;

        $('.ContentColumn').html(Core.Template.Render("Agent/WidgetLoading"));

        // Update BreadCrumb
        $('ul.BreadCrumb li:nth-child(2)').nextAll().remove();
        BreadCrumbItems = Selected.split("::");

        if (Core.Config.Get('Action') == 'AgentPreferences') {
            URL += "?Action=AgentPreferences;Subaction=Group;Group=Advanced;RootNavigation=";
        }
        else {
            URL += "?Action=AdminSystemConfigurationGroup;RootNavigation=";
        }
        for (Index in BreadCrumbItems) {
            if (Index != 0) {
                URL += "::";
            }

            URL += BreadCrumbItems[Index];
            ListItem = Core.Template.Render("SysConfig/BreadCrumbItem", {
                Title: BreadCrumbItems[Index],
                URL: URL
            });
            $('ul.BreadCrumb').append(ListItem);
        }

        if (Core.Config.Get('Action') == 'AgentPreferences') {
            Action = "AgentPreferences";
        }

        window.history.pushState(null, null, URL);

        Data = {
            Action     : Action,
            Subaction  : 'SettingList',
            Category   : $('#Category').val(),
            RootNavigation : Selected
        };

        if (Core.Config.Get('Action') == 'AgentPreferences') {
            Data["IsValid"] = 1;
            // TODO: Implement - right now it goes always to the Advanced group.
            Data["Group"] = 'Advanced';
        }

        if (!$('#ConfigTree').length) {
            return;
        }

        Core.AJAX.FunctionCall(
            Core.Config.Get('CGIHandle'),
            Data,
            function (HTML) {

                $(".ContentColumn").html(HTML);
                TargetNS.Init();
                if (Core.Config.Get('Action') == 'AgentPreferences') {
                    Core.Agent.Preferences.InitSysConfig();
                    Core.UI.Table.InitTableFilter($("#FilterSettings"), $(".SettingsList"));
                }
                else {
                    Core.Agent.Admin.SystemConfiguration.InitGroupView(true);
                }

                // check if we need to show/hide the help and dynamic actions widget
                if ($('.WidgetSimple .Setting').length) {
                    $('#UserWidgetState_SystemConfiguration_Help, #UserWidgetState_SystemConfiguration_Sticky').removeClass('Hidden');
                }
                else {
                    $('#UserWidgetState_SystemConfiguration_Help, #UserWidgetState_SystemConfiguration_Sticky').addClass('Hidden');
                }
                Core.UI.InitStickyElement();

                Core.App.Publish('SystemConfiguration.SettingListUpdate');

                Core.UI.InputFields.InitSelect(
                    $(".SettingsList .Modernize")
                );

                // scroll to top to see all the settings from the current node
                $('html, body').animate({
                    scrollTop: $('.ContentColumn .WidgetSimple').first().position().top
                }, 'fast');
            }, 'html'
        );

        if ($('#SysConfigFavourites').length) {
            window.setTimeout(function() {
                Core.Agent.Admin.SystemConfiguration.InitFavourites();
            }, 1000);
        }
    }

    Core.Init.RegisterNamespace(TargetNS, 'APP_MODULE');

    return TargetNS;
}(Core.SystemConfiguration || {}));
